Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4a28e22
refactor: split background script into modular components with TypeSc…
ptgms Sep 22, 2025
145eb11
fix: make firefox background script work
ptgms Sep 22, 2025
1412bb1
feat: improve error handling with detailed error messages and UI updates
ptgms Sep 30, 2025
c2cc935
feat: add multi-language support
ptgms Sep 30, 2025
6f297e1
remove src folder if exists
ptgms Sep 30, 2025
ba08347
defer script, rename variable
Oct 21, 2025
8aeb951
add document interface
Oct 21, 2025
78a8b53
html
Oct 21, 2025
d95fc5a
tmp
Oct 21, 2025
60091f4
merge code
Oct 21, 2025
65f02a8
fix types, remove any
Oct 21, 2025
f7efcf9
add more types
Oct 21, 2025
e534bef
revert serviceresponse id number to string
Oct 21, 2025
4381737
clean
Oct 21, 2025
de4ffca
Group documents wihtout points
Oct 21, 2025
6547ea4
fix points without docs and docs without points always showing
Oct 21, 2025
1c86882
fix type error
Oct 21, 2025
39b074a
fix typo
Oct 21, 2025
e12cb1a
add pointListStyle options to settings page
shadowwwind Oct 22, 2025
14e8670
add default docStyle
shadowwwind Oct 22, 2025
0219c4d
check pointListStyle before rendering
shadowwwind Oct 22, 2025
405e067
re-add the unified pointsList
shadowwwind Oct 22, 2025
b55d34e
clean up
shadowwwind Oct 22, 2025
15a01fd
fix Unexpected negated condition.
shadowwwind Oct 22, 2025
54aa075
use for of
shadowwwind Oct 22, 2025
132a165
add alt text to icon
shadowwwind Oct 22, 2025
5fd853c
clean
shadowwwind Oct 22, 2025
775d3b1
move a constant to constants
ptgms Oct 29, 2025
b944b98
fix: remove dupped id's and unused elements, change style selector to…
shadowwwind Nov 4, 2025
b11a284
Merge branch 'tosdr:modules' into modules
shadowwwind Nov 18, 2025
1c327fb
[feat] sort docuements alphabetically
shadowwwind Nov 18, 2025
31557cc
[fix] move default listStyle to constants, improve types, fix typo
shadowwwind Nov 18, 2025
ac1986a
Merge branch 'master' into modules
shadowwwind Nov 27, 2025
81f92ed
merge
shadowwwind Nov 27, 2025
cedecc8
Merge pull request #1 from shadowwwind/modules
shadowwwind Nov 27, 2025
585a2fd
add a list of documents with point summary to the top
shadowwwind Dec 2, 2025
cf45e3f
Merge pull request #2 from shadowwwind/master
shadowwwind Dec 2, 2025
fb35744
fix typo
shadowwwind Dec 2, 2025
2ff70d6
add comments
shadowwwind Dec 2, 2025
933957c
add comments, remove unused element
shadowwwind Dec 2, 2025
5492575
Merge pull request #3 from shadowwwind/master
shadowwwind Dec 2, 2025
15a1c18
typo
shadowwwind Dec 2, 2025
3cfbc0b
Merge branch 'master' into modules
shadowwwind Dec 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/scripts/background/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { setLocal } from '../lib/chromeStorage';
import { checkIfUpdateNeeded } from './database';
import { donationReminderAllowed } from './donation';
import { initializePageAction } from './pageAction';
import { DEFAULT_LIST_STYLE } from "../constants";

export async function handleExtensionInstalled(reason:chrome.runtime.InstalledDetails): Promise<void> {
const donationAllowed = donationReminderAllowed(navigator.userAgent);
Expand All @@ -13,6 +14,7 @@ export async function handleExtensionInstalled(reason:chrome.runtime.InstalledDe
active: false,
allowedPlattform: donationAllowed,
},
pointListStyle: DEFAULT_LIST_STYLE
});

await checkIfUpdateNeeded(true, reason);
Expand Down
4 changes: 3 additions & 1 deletion src/scripts/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ export const API_HEADERS = {
apikey: atob('Y29uZ3JhdHMgb24gZ2V0dGluZyB0aGUga2V5IDpQ'),
};

export const SUPPORTED_LANGUAGES = ['en', 'de', 'nl', 'fr', 'es'] as const;
export const SUPPORTED_LANGUAGES = ['en', 'de', 'nl', 'fr', 'es'] as const;

export const DEFAULT_LIST_STYLE :"docCategories" | "unified" = "docCategories" as const;
302 changes: 240 additions & 62 deletions src/scripts/views/popup/service.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { getApiUrl, getLanguage, isCuratorMode } from './state';
import { getApiUrl, getLanguage, isCuratorMode, getPointListStyle } from './state';
import { applyHeaderColor } from './theme';

interface ServicePoint {
status: string;
title: string;
case?: {
case: {
classification?: string;
localized_title?: string | null;
};
document_id?: number
}

interface ServiceDocument {
id: number
name: string
url: string
}

interface ServiceResponse {
name: string;
rating?: string;
points: ServicePoint[];
documents: ServiceDocument[]
}

interface SearchResponse {
Expand All @@ -23,6 +31,13 @@ interface SearchResponse {
}>;
}

interface FilteredPoints {
blocker: ServicePoint[];
bad: ServicePoint[];
good: ServicePoint[];
neutral: ServicePoint[];
}

export async function displayServiceDetails(
id: string,
options: { unverified?: boolean } = {}
Expand Down Expand Up @@ -52,7 +67,17 @@ export async function displayServiceDetails(
updatePointsCount(data.points.length);
revealLoadedState(options.unverified === true);

populateList(data.points);
const pointListStyle = getPointListStyle() //check the user preferences to show the list accordingly

if (pointListStyle === "docCategories") {
populateListDocCategories(data.points, data.documents);
} else if (pointListStyle === "unified") {
populateListUnified(data.points)
} else {
console.error("Unsupported pointListStyle", pointListStyle);
}


} catch (error) {
hideLoadingState();
showErrorOverlay(
Expand Down Expand Up @@ -164,7 +189,18 @@ function revealLoadedState(unverified: boolean): void {
}
}

function populateList(points: ServicePoint[]): void {
function populateListUnified(allPoints: ServicePoint[]) {
const documentList = document.getElementById('documentList');
const doc = document.createElement('div');
const temp = `
<div class="">
<div id="pointList" class="pointList">
</div>
</div>`
;
doc.innerHTML = temp.trim();
documentList!.appendChild(doc.firstChild!);

const pointsList = document.getElementById('pointList');
if (!pointsList) {
return;
Expand All @@ -173,79 +209,221 @@ function populateList(points: ServicePoint[]): void {
pointsList.style.display = 'block';
pointsList.innerHTML = '';

const filteredPoints = filterPoints(points);
const filteredPoints = filterPoints(allPoints);

appendPointGroup(filteredPoints.blocker, pointsList, false);
appendPointGroup(filteredPoints.bad, pointsList, false);
appendPointGroup(filteredPoints.good, pointsList, false);
appendPointGroup(filteredPoints.neutral, pointsList, true);
createPointList(filteredPoints.blocker, pointsList, false);
createPointList(filteredPoints.bad, pointsList, false);
createPointList(filteredPoints.good, pointsList, false);
createPointList(filteredPoints.neutral, pointsList, true);
}

function filterPoints(points: ServicePoint[]): {
blocker: ServicePoint[];
bad: ServicePoint[];
good: ServicePoint[];
neutral: ServicePoint[];
} {
const curatedPoints = points.filter((point) => {
if (!isCuratorMode()) {
return point.status === 'approved';
}
return point.status === 'approved' || point.status === 'pending';
});

return {
blocker: curatedPoints.filter(
(point) => point.case?.classification === 'blocker'
),
bad: curatedPoints.filter(
(point) => point.case?.classification === 'bad'
),
good: curatedPoints.filter(
(point) => point.case?.classification === 'good'
),
neutral: curatedPoints.filter(
(point) => point.case?.classification === 'neutral'
),
};
}

function appendPointGroup(
points: ServicePoint[],
container: HTMLElement,
isLastGroup: boolean
): void {
let added = 0;
function populateListDocCategories(allPoints: ServicePoint[], documents: ServiceDocument[]) {
const documentList = document.getElementById('documentList');
//sort documents alphabetically
try {
documents.sort((a, b) =>
a.name.localeCompare(b.name)
)
} catch (error) {
console.warn(error)
}

points.forEach((point, index) => {
const wrapper = document.createElement('div');
const classification = point.case?.classification ?? 'neutral';
const pointTitle = point.case?.localized_title ?? point.title;
wrapper.innerHTML = `
<div class="point ${classification}">
<img src="icons/${classification}.svg">
<p>${pointTitle}</p>
${renderCuratorTag(point.status)}
// prepare the Document Index
const indexElement = document.getElementById("documentIndex") as HTMLElement
indexElement.style = "display:block"

// Split points by Document and display them seperatly
for (let i of documents) {
const element = i;

const docPoints = allPoints.filter((point:ServicePoint) => point.document_id === element.id) //only points of the current document
const sortedPoints = filterPoints(docPoints)


addDocumentToIndex(element, indexElement, sortedPoints)

// check if the document has points and either display it with the points else add them to the bottom
if (sortedPoints.blocker.length + sortedPoints.bad.length + sortedPoints.neutral.length + sortedPoints.good.length > 0) { //documents with points
const doc = document.createElement('div');
const temp = `
<div class="">
<div class="documentHeader">
<h3 id="documents_${element.id}" class="documentTitle" >${element.name}</h3>
<a href="${element.url}" target="_blank">Read Original</a>
</div>
<div id="pointList_${element.id}" class="pointList">
<a style="display: none">...</a>
</div>
</div>`;
doc.innerHTML = temp.trim();
documentList!.appendChild(doc.firstChild!);

const pointsList = document.getElementById(`pointList_${element.id}`)!

createSortedPoints(sortedPoints,pointsList)
} else { //documents without points
const docsWithoutPointsWraper = document.getElementById('docsWithoutPointsWraper')
const docsWithoutPoints = document.getElementById('docsWithoutPoints')

if (docsWithoutPoints?.style.display === "none") {
docsWithoutPoints.style.display = "block"
}
const doc = document.createElement('div');
const temp = `
<div class="documentHeader">
<h3 class="documentTitle" id="documents_${element.id}">${element.name}</h3>
<a href="${element.url}" target="_blank">Read Original></a>
</div>`;
doc.innerHTML = temp.trim();
docsWithoutPointsWraper!.appendChild(doc.firstChild!);
}
}
//display points not linked to a document
const noDocPoints = allPoints.filter((point: ServicePoint) => point.document_id === undefined)
if (noDocPoints.length > 0) {
const doc = document.createElement('div');
const temp = `
<div class="">
<div class="documentHeader">
<h3 class="documentTitle">Points not linked to a Document</h3>
</div>
`.trim();
if (wrapper.firstChild) {
container.appendChild(wrapper.firstChild as HTMLElement);
<div id="pointList_unlinkedPoints" class="pointList">
<a style="display: none">...</a>
</div>
</div>`;
doc.innerHTML = temp.trim();
documentList!.appendChild(doc.firstChild!);
const sortedPoints = filterPoints(noDocPoints)
const pointsList = document.getElementById(`pointList_unlinkedPoints`)!
createSortedPoints(sortedPoints,pointsList)

}
}
function filterPoints(points:ServicePoint[]) {
if (isCuratorMode()) {
points = points.filter(
(point) =>
point.status === 'approved' || point.status === 'pending'
);
} else {
points = points.filter((point) => point.status === 'approved');
}
added += 1;
let filteredPoints:FilteredPoints = {
blocker: [],
bad: [],
good: [],
neutral: []
}
filteredPoints.blocker = points.filter(
(point) => point.case.classification === 'blocker'
);
filteredPoints.bad = points.filter(
(point) => point.case.classification === 'bad'
);
filteredPoints.good = points.filter(
(point) => point.case.classification === 'good'
);
filteredPoints.neutral = points.filter(
(point) => point.case.classification === 'neutral'
);
return filteredPoints
}

function createSortedPoints(sortedPoints:FilteredPoints,pointsList:HTMLElement) {
if (sortedPoints.blocker) {
createPointList(sortedPoints.blocker, pointsList, false);
}
if (sortedPoints.bad) {
createPointList(sortedPoints.bad, pointsList, false);
}
if (sortedPoints.good) {
createPointList(sortedPoints.good, pointsList, false);
}
if (sortedPoints.neutral) {
createPointList(sortedPoints.neutral, pointsList, true);
}
}

if (index !== points.length - 1) {
function createPointList(pointsFiltered: ServicePoint[], pointsList: HTMLElement, last: boolean) {
let added = 0;
for (let i = 0; i < pointsFiltered.length; i++) {
const point = document.createElement('div');
const pointTitle = pointsFiltered[i]!.case?.localized_title ?? pointsFiltered[i]!.title;

let temp = `
<div class="point ${pointsFiltered[i]!.case.classification}">
<img src="icons/${pointsFiltered[i]!.case.classification}.svg">
<p>${pointTitle}</p>
${renderCuratorTag(pointsFiltered[i]!.status)}
</div>`;
point.innerHTML = temp.trim();
pointsList.appendChild(point.firstChild!);
added++;
if (i !== pointsFiltered.length - 1) {
const divider = document.createElement('hr');
container.appendChild(divider);
pointsList.appendChild(divider);
}
});

if (added > 0 && !isLastGroup) {
}
if (added !== 0 && !last) {
const divider = document.createElement('hr');
divider.classList.add('group');
container.appendChild(divider);
pointsList.appendChild(divider);
}
}
function addDocumentToIndex(serviceDocument:ServiceDocument, indexElement:HTMLElement, points:FilteredPoints) { //creates an index of the documents
const list = document.createElement('li')
const item = document.createElement('a')

const pointSummary = documentIndexPointSummary(points)

item.innerText = serviceDocument.name
item.href = `#documents_${serviceDocument.id}`


list.appendChild(item)
list.appendChild(pointSummary)

indexElement.appendChild(list)

function documentIndexPointSummary(points:FilteredPoints) { //adds the number of points of each classification as a summary below the document index entry
const pointsSummanry = document.createElement("div")
pointsSummanry.classList = "indexSummaryWraper"

createSummary("good")
createSummary("neutral")
createSummary("bad")
createSummary("blocker")

return pointsSummanry

function createSummary(classification:"good" | "bad" | "neutral" | "blocker") {
const wrapper = document.createElement("div")
const count = points[classification].length

//add the classification icon
const img = document.createElement("img");
img.src = `icons/${classification}.svg`

wrapper.appendChild(img)


//add number of points of classification
const div = document.createElement("div");
div.innerText = `${count}`;
div.classList.add("indexSummary", classification);

wrapper.appendChild(div)

pointsSummanry.appendChild(wrapper)
}
}
}





function renderCuratorTag(status: string): string {
if (!isCuratorMode() || status === 'approved') {
return '';
Expand Down
Loading