diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 00000000..82574019 Binary files /dev/null and b/bun.lockb differ diff --git a/src/home/authentication.ts b/src/home/authentication.ts index 01ff446f..4e8fb1ae 100644 --- a/src/home/authentication.ts +++ b/src/home/authentication.ts @@ -4,7 +4,6 @@ import { getGitHubUser } from "./getters/get-github-user"; import { GitHubUser } from "./github-types"; import { displayGitHubUserInformation } from "./rendering/display-github-user-information"; import { renderGitHubLoginButton } from "./rendering/render-github-login-button"; -import { viewToggle } from "./fetch-github/fetch-and-display-previews"; export async function authentication() { const accessToken = await getGitHubAccessToken(); @@ -16,6 +15,5 @@ export async function authentication() { if (gitHubUser) { trackDevRelReferral(gitHubUser.login + "|" + gitHubUser.id); await displayGitHubUserInformation(gitHubUser); - viewToggle.disabled = false; } } diff --git a/src/home/fetch-github/fetch-and-display-previews.ts b/src/home/fetch-github/fetch-and-display-previews.ts index 3719b7ec..2b70c3c8 100644 --- a/src/home/fetch-github/fetch-and-display-previews.ts +++ b/src/home/fetch-github/fetch-and-display-previews.ts @@ -1,21 +1,19 @@ import { getGitHubAccessToken } from "../getters/get-github-access-token"; import { getImageFromCache } from "../getters/get-indexed-db"; import { getLocalStore } from "../getters/get-local-store"; -import { GITHUB_TASKS_STORAGE_KEY, TaskStorageItems } from "../github-types"; +import { GITHUB_TASKS_STORAGE_KEY, GitHubIssue, TaskStorageItems } from "../github-types"; import { taskManager } from "../home"; import { applyAvatarsToIssues, renderGitHubIssues } from "../rendering/render-github-issues"; import { Sorting } from "../sorting/generate-sorting-buttons"; import { sortIssuesController } from "../sorting/sort-issues-controller"; import { fetchAvatar } from "./fetch-avatar"; import { organizationImageCache } from "./fetch-issues-full"; -import { fetchIssuePreviews } from "./fetch-issues-preview"; -import { TaskMaybeFull, TaskNoFull, TaskWithFull } from "./preview-to-full-mapping"; export type Options = { ordering: "normal" | "reverse"; }; -let isProposalOnlyViewer = false; // or proposal viewer +let isProposalOnlyViewer = false; export const viewToggle = document.getElementById("view-toggle") as HTMLInputElement; if (!viewToggle) { @@ -31,19 +29,16 @@ export async function fetchAndDisplayPreviewsFromCache(sorting?: Sorting, option let _cachedTasks = getLocalStore(GITHUB_TASKS_STORAGE_KEY) as TaskStorageItems; const _accessToken = await getGitHubAccessToken(); - // Refresh the storage if there is no logged-in object in cachedTasks but there is one now. if (_cachedTasks && !_cachedTasks.loggedIn && _accessToken) { localStorage.removeItem(GITHUB_TASKS_STORAGE_KEY); - return fetchAndDisplayPreviewsFromNetwork(sorting, options); + return fetchAndDisplayIssuesFromNetwork(sorting, options); } - // If previously logged in but not anymore, clear cache and fetch from network. if (_cachedTasks && _cachedTasks.loggedIn && !_accessToken) { localStorage.removeItem(GITHUB_TASKS_STORAGE_KEY); - return fetchAndDisplayPreviewsFromNetwork(sorting, options); + return fetchAndDisplayIssuesFromNetwork(sorting, options); } - // makes sure tasks have a timestamp to know how old the cache is, or refresh if older than 15 minutes if (!_cachedTasks || !_cachedTasks.timestamp || _cachedTasks.timestamp + 60 * 1000 * 15 <= Date.now()) { _cachedTasks = { timestamp: Date.now(), @@ -52,34 +47,27 @@ export async function fetchAndDisplayPreviewsFromCache(sorting?: Sorting, option }; } - const cachedTasks = _cachedTasks.tasks as TaskMaybeFull[]; + const cachedTasks = _cachedTasks.tasks; taskManager.syncTasks(cachedTasks); if (!cachedTasks.length) { - // load from network if there are no cached issues - return fetchAndDisplayPreviewsFromNetwork(sorting, options); + return fetchAndDisplayIssuesFromNetwork(sorting, options); } else { displayGitHubIssues(sorting, options); return fetchAvatars(); } } -export async function fetchAndDisplayPreviewsFromNetwork(sorting?: Sorting, options = { ordering: "normal" }) { - const fetchedPreviews = await fetchIssuePreviews(); - const cachedTasks = taskManager.getTasks(); - const updatedCachedIssues = verifyGitHubIssueState(cachedTasks, fetchedPreviews); - taskManager.syncTasks(updatedCachedIssues); +export async function fetchAndDisplayIssuesFromNetwork(sorting?: Sorting, options = { ordering: "normal" }) { displayGitHubIssues(sorting, options); return fetchAvatars(); } export async function fetchAvatars() { const cachedTasks = taskManager.getTasks(); - const urlPattern = /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)/; - const avatarPromises = cachedTasks.map(async (task) => { - const match = task.preview.body?.match(urlPattern); - const orgName = match?.groups?.org; + const avatarPromises = cachedTasks.map(async (task: GitHubIssue) => { + const [orgName] = task.repository_url.split("/").slice(-2); if (orgName) { return fetchAvatar(orgName); } @@ -88,68 +76,36 @@ export async function fetchAvatars() { await Promise.allSettled(avatarPromises); applyAvatarsToIssues(); - return cachedTasks; -} - -export function taskWithFullTest(task: TaskNoFull | TaskWithFull): task is TaskWithFull { - return (task as TaskWithFull).full !== null && (task as TaskWithFull).full !== undefined; -} - -export function verifyGitHubIssueState(cachedTasks: TaskMaybeFull[], fetchedPreviews: TaskNoFull[]): (TaskNoFull | TaskWithFull)[] { - return fetchedPreviews.map((fetched) => { - const cachedTask = cachedTasks.find((c) => c.full?.id === fetched.preview.id); - if (cachedTask) { - if (taskWithFullTest(cachedTask)) { - const cachedFullIssue = cachedTask.full; - const task = { ...fetched, full: cachedFullIssue }; - return task; - } else { - // no full issue in task - } - } else { - // no cached task - } - return { - preview: fetched.preview, - } as TaskNoFull; - }); } export function displayGitHubIssues(sorting?: Sorting, options = { ordering: "normal" }) { - // Load avatars from cache - const urlPattern = /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)/; - const cachedTasks = taskManager.getTasks(); - cachedTasks.forEach(async ({ preview }) => { - if (!preview.body) { - throw new Error(`Preview body is undefined for task with id: ${preview.id}`); - } - const match = preview.body.match(urlPattern); - const orgName = match?.groups?.org; - if (orgName) { - const avatarUrl = await getImageFromCache({ dbName: "GitHubAvatars", storeName: "ImageStore", orgName: `avatarUrl-${orgName}` }); - if (avatarUrl) { - organizationImageCache.set(orgName, avatarUrl); - } - } + const cached = taskManager.getTasks(); + cached.forEach(async (gitHubIssue) => { + const [orgName] = gitHubIssue.repository_url.split("/").slice(-2); + + getImageFromCache({ + dbName: "GitHubAvatars", + storeName: "ImageStore", + orgName: `avatarUrl-${orgName}`, + }) + .then((avatarUrl) => organizationImageCache.set(orgName, avatarUrl)) + .catch(console.error); }); - // Render issues - const sortedIssues = sortIssuesController(cachedTasks, sorting, options); + const sortedIssues = sortIssuesController(cached, sorting, options); const sortedAndFiltered = sortedIssues.filter(getProposalsOnlyFilter(isProposalOnlyViewer)); renderGitHubIssues(sortedAndFiltered); } function getProposalsOnlyFilter(getProposals: boolean) { - return (task: TaskMaybeFull) => { - if (!task.full?.labels) return false; + return (issue: GitHubIssue) => { + if (!issue?.labels) return false; - const hasPriceLabel = task.full.labels.some((label) => { + const hasPriceLabel = issue.labels.some((label) => { if (typeof label === "string") return false; - return label.name?.startsWith("Price: ") || label.name?.startsWith("Pricing: "); + return label.name?.startsWith("Price: ") || label.name?.startsWith("Price: "); }); - // If getProposals is true, we want tasks WITHOUT price labels - // If getProposals is false, we want tasks WITH price labels return getProposals ? !hasPriceLabel : hasPriceLabel; }; } diff --git a/src/home/fetch-github/fetch-issues-full.ts b/src/home/fetch-github/fetch-issues-full.ts index b10df1b4..c5bc6390 100644 --- a/src/home/fetch-github/fetch-issues-full.ts +++ b/src/home/fetch-github/fetch-issues-full.ts @@ -1,49 +1,8 @@ -import { Octokit } from "@octokit/rest"; -import { getGitHubAccessToken } from "../getters/get-github-access-token"; import { GitHubIssue } from "../github-types"; -import { taskWithFullTest } from "./fetch-and-display-previews"; -import { fetchAvatar } from "./fetch-avatar"; -import { TaskMaybeFull, TaskWithFull } from "./preview-to-full-mapping"; - export const organizationImageCache = new Map(); -export async function fetchIssuesFull(taskPreviews: TaskMaybeFull[]): Promise { - const octokit = new Octokit({ auth: await getGitHubAccessToken() }); - const urlPattern = /https:\/\/github\.com\/(?[^/]+)\/(?[^/]+)\/issues\/(?\d+)/; - - const fullTaskPromises = taskPreviews.map(async (task) => { - const match = task.preview.body.match(urlPattern); - - if (!match || !match.groups) { - console.error("Invalid issue body URL format"); - return Promise.resolve(null); - } - - const { org, repo, issue_number } = match.groups; - - const { data: response } = await octokit.request("GET /repos/{org}/{repo}/issues/{issue_number}", { issue_number, repo, org }); - - task.full = response as GitHubIssue; - - const urlMatch = task.full.html_url.match(urlPattern); - const orgName = urlMatch?.groups?.org; - if (orgName) { - await fetchAvatar(orgName); - } - const isTaskWithFull = taskWithFullTest(task); - - if (isTaskWithFull) { - return task; - } else { - throw new Error("Task is not a TaskWithFull"); - } - }); - - const settled = await Promise.allSettled(fullTaskPromises); - const fullTasks = settled - .filter((result): result is PromiseFulfilledResult => result.status === "fulfilled") - .map((result) => result.value) - .filter((issue): issue is TaskWithFull => issue !== null); - - return fullTasks; +export async function fetchIssuesFull(): Promise { + const response = await fetch("https://raw.githubusercontent.com/ubiquity/devpool-directory/refs/heads/development/devpool-issues.json"); + const jsonData = await response.json(); + return jsonData; } diff --git a/src/home/fetch-github/fetch-issues-preview.ts b/src/home/fetch-github/fetch-issues-preview.ts index b2c6cccc..01fc453d 100644 --- a/src/home/fetch-github/fetch-issues-preview.ts +++ b/src/home/fetch-github/fetch-issues-preview.ts @@ -1,104 +1,4 @@ -import { RequestError } from "@octokit/request-error"; -import { Octokit } from "@octokit/rest"; -import { getGitHubAccessToken, getGitHubUserName } from "../getters/get-github-access-token"; -import { GitHubIssue } from "../github-types"; import { displayPopupMessage } from "../rendering/display-popup-modal"; -import { handleRateLimit } from "./handle-rate-limit"; -import { TaskNoFull } from "./preview-to-full-mapping"; - -async function checkPrivateRepoAccess(): Promise { - const octokit = new Octokit({ auth: await getGitHubAccessToken() }); - const username = getGitHubUserName(); - - if (username) { - try { - const response = await octokit.repos.checkCollaborator({ - owner: "ubiquity", - repo: "devpool-directory-private", - username, - }); - - if (response.status === 204) { - // If the response is successful, it means the user has access to the private repository - return true; - } - return false; - } catch (error) { - if (!!error && typeof error === "object" && "status" in error && (error.status === 404 || error.status === 401)) { - // If the status is 404, it means the user is not a collaborator, hence no access - return false; - } else { - // Handle other errors if needed - console.error("Error checking repository access:", error); - throw error; - } - } - } - - return false; -} - -export async function fetchIssuePreviews(): Promise { - const octokit = new Octokit({ auth: await getGitHubAccessToken() }); - let freshIssues: GitHubIssue[] = []; - let hasPrivateRepoAccess = false; // Flag to track access to the private repository - - try { - // Check if the user has access to the private repository - hasPrivateRepoAccess = await checkPrivateRepoAccess(); - - // Fetch issues from public repository - const publicResponse = await octokit.paginate(octokit.issues.listForRepo, { - owner: "ubiquity", - repo: "devpool-directory", - state: "open", - }); - - const publicIssues = publicResponse.filter((issue: GitHubIssue) => !issue.pull_request); - - // Fetch issues from the private repository only if the user has access - if (hasPrivateRepoAccess) { - await fetchPrivateIssues(publicIssues); - } else { - // If user doesn't have access, only load issues from the public repository - freshIssues = publicIssues; - } - } catch (error) { - if (!!error && typeof error === "object" && "status" in error && error.status === 403) { - await handleRateLimit(octokit, error as RequestError); - } else { - throw error; - } - } - - const tasks = freshIssues.map((preview: GitHubIssue) => ({ - preview: preview, - full: null, - isNew: true, - isModified: true, - })) as TaskNoFull[]; - - return tasks; - - async function fetchPrivateIssues(publicIssues: GitHubIssue[]) { - const privateResponse = await octokit.paginate(octokit.issues.listForRepo, { - owner: "ubiquity", - repo: "devpool-directory-private", - state: "open", - }); - const privateIssues = privateResponse.filter((issue: GitHubIssue) => !issue.pull_request); - - // Mark private issues - // TODO: indicate private issues in the UI - - // const privateIssuesWithFlag = privateIssues.map((issue) => { - // return issue; - // }); - - // Combine public and private issues - freshIssues = [...privateIssues, ...publicIssues]; - } -} export function rateLimitModal(message: string) { displayPopupMessage({ modalHeader: `GitHub API rate limit exceeded.`, modalBody: message, isError: false }); diff --git a/src/home/fetch-github/handle-rate-limit.ts b/src/home/fetch-github/handle-rate-limit.ts index bdb7c08f..9f4367e0 100644 --- a/src/home/fetch-github/handle-rate-limit.ts +++ b/src/home/fetch-github/handle-rate-limit.ts @@ -1,11 +1,11 @@ import { RequestError } from "@octokit/request-error"; import { Octokit } from "@octokit/rest"; import { getGitHubUser } from "../getters/get-github-user"; +import { toolbar } from "../ready-toolbar"; import { renderErrorInModal } from "../rendering/display-popup-modal"; -import { rateLimitModal } from "./fetch-issues-preview"; import { gitHubLoginButton } from "../rendering/render-github-login-button"; -import { preview } from "../rendering/render-preview-modal"; -import { toolbar } from "../ready-toolbar"; +import { modal } from "../rendering/render-preview-modal"; +import { rateLimitModal } from "./fetch-issues-preview"; type RateLimit = { reset: number | null; @@ -18,7 +18,7 @@ export async function handleRateLimit(octokit?: Octokit, error?: RequestError) { user: false, }; - preview.classList.add("active"); + modal.classList.add("active"); document.body.classList.add("preview-active"); if (toolbar) { diff --git a/src/home/fetch-github/preview-to-full-mapping.ts b/src/home/fetch-github/preview-to-full-mapping.ts index f36eac24..8e4a86ce 100644 --- a/src/home/fetch-github/preview-to-full-mapping.ts +++ b/src/home/fetch-github/preview-to-full-mapping.ts @@ -4,24 +4,3 @@ export type TaskNoState = { preview: GitHubIssue; full: null | GitHubIssue; }; - -export type TaskNoFull = { - preview: GitHubIssue; - full: null; - isNew: boolean; - isModified: boolean; -}; - -export type TaskMaybeFull = { - preview: GitHubIssue; - full: null | GitHubIssue; - isNew: boolean; - isModified: boolean; -}; - -export type TaskWithFull = { - preview: GitHubIssue; - full: GitHubIssue; - isNew: boolean; - isModified: boolean; -}; diff --git a/src/home/github-types.ts b/src/home/github-types.ts index 89d34e93..33307cba 100644 --- a/src/home/github-types.ts +++ b/src/home/github-types.ts @@ -1,5 +1,4 @@ import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods"; -import { TaskNoState } from "./fetch-github/preview-to-full-mapping"; export interface AvatarCache { [organization: string]: string | null; @@ -9,10 +8,21 @@ export const GITHUB_TASKS_STORAGE_KEY = "gitHubTasks"; export type TaskStorageItems = { timestamp: number; - tasks: TaskNoState[]; + tasks: GitHubIssue[]; loggedIn: boolean; }; export type GitHubUserResponse = RestEndpointMethodTypes["users"]["getByUsername"]["response"]; export type GitHubUser = GitHubUserResponse["data"]; export type GitHubIssue = RestEndpointMethodTypes["issues"]["get"]["response"]["data"]; +export type GitHubLabel = + | { + id?: number; + node_id?: string; + url?: string; + name: string; + description?: string | null; + color?: string | null; + default?: boolean; + } + | string; diff --git a/src/home/home.ts b/src/home/home.ts index ae120b95..259370bf 100644 --- a/src/home/home.ts +++ b/src/home/home.ts @@ -1,11 +1,12 @@ import { grid } from "../the-grid"; import { authentication } from "./authentication"; import { initiateDevRelTracking } from "./devrel-tracker"; -import { displayGitHubIssues, fetchAndDisplayPreviewsFromCache } from "./fetch-github/fetch-and-display-previews"; +import { fetchAvatars, displayGitHubIssues } from "./fetch-github/fetch-and-display-previews"; import { fetchIssuesFull } from "./fetch-github/fetch-issues-full"; import { readyToolbar } from "./ready-toolbar"; +import { registerServiceWorker } from "./register-service-worker"; +import { renderServiceMessage } from "./render-service-message"; import { renderErrorInModal } from "./rendering/display-popup-modal"; -import { applyAvatarsToIssues } from "./rendering/render-github-issues"; import { renderGitRevision } from "./rendering/render-github-login-button"; import { generateSortingToolbar } from "./sorting/generate-sorting-buttons"; import { TaskManager } from "./task-manager"; @@ -36,39 +37,14 @@ export const taskManager = new TaskManager(container); void (async function home() { void authentication(); void readyToolbar(); - const previews = await fetchAndDisplayPreviewsFromCache(); - const fullTasks = await fetchIssuesFull(previews); - taskManager.syncTasks(fullTasks); - await taskManager.writeToStorage(); + const gitHubIssues = await fetchIssuesFull(); + taskManager.syncTasks(gitHubIssues); + void fetchAvatars(); + void displayGitHubIssues(); if ("serviceWorker" in navigator) { - window.addEventListener("load", () => { - navigator.serviceWorker.register("/dist/src/progressive-web-app.js").then( - (registration) => { - console.log("ServiceWorker registration successful with scope: ", registration.scope); - }, - (err) => { - console.log("ServiceWorker registration failed: ", err); - } - ); - }); + registerServiceWorker(); } - if (!container.childElementCount) { - displayGitHubIssues(); - applyAvatarsToIssues(); - } - return fullTasks; + return gitHubIssues; })(); - -function renderServiceMessage() { - const urlParams = new URLSearchParams(window.location.search); - const message = urlParams.get("message"); - if (message) { - const serviceMessageContainer = document.querySelector("#bottom-bar > div"); - if (serviceMessageContainer) { - serviceMessageContainer.textContent = message; - serviceMessageContainer.parentElement?.classList.add("ready"); - } - } -} diff --git a/src/home/register-service-worker.ts b/src/home/register-service-worker.ts new file mode 100644 index 00000000..9301a322 --- /dev/null +++ b/src/home/register-service-worker.ts @@ -0,0 +1,12 @@ +export function registerServiceWorker() { + window.addEventListener("load", () => { + navigator.serviceWorker.register("/dist/src/progressive-web-app.js").then( + (registration) => { + console.log("ServiceWorker registration successful with scope: ", registration.scope); + }, + (err) => { + console.log("ServiceWorker registration failed: ", err); + } + ); + }); +} diff --git a/src/home/render-service-message.ts b/src/home/render-service-message.ts new file mode 100644 index 00000000..20029283 --- /dev/null +++ b/src/home/render-service-message.ts @@ -0,0 +1,11 @@ +export function renderServiceMessage() { + const urlParams = new URLSearchParams(window.location.search); + const message = urlParams.get("message"); + if (message) { + const serviceMessageContainer = document.querySelector("#bottom-bar > div"); + if (serviceMessageContainer) { + serviceMessageContainer.textContent = message; + serviceMessageContainer.parentElement?.classList.add("ready"); + } + } +} diff --git a/src/home/rendering/display-popup-modal.ts b/src/home/rendering/display-popup-modal.ts index 71c76e44..02831dbf 100644 --- a/src/home/rendering/display-popup-modal.ts +++ b/src/home/rendering/display-popup-modal.ts @@ -1,13 +1,13 @@ import { toolbar } from "../ready-toolbar"; -import { preview, previewBodyInner, titleAnchor, titleHeader } from "./render-preview-modal"; +import { modal, modalBodyInner, titleAnchor, titleHeader } from "./render-preview-modal"; export function displayPopupMessage({ modalHeader, modalBody, isError, url }: { modalHeader: string; modalBody: string; isError: boolean; url?: string }) { titleHeader.textContent = modalHeader; if (url) { titleAnchor.href = url; } - previewBodyInner.innerHTML = modalBody; + modalBodyInner.innerHTML = modalBody; - preview.classList.add("active"); + modal.classList.add("active"); document.body.classList.add("preview-active"); if (toolbar) { @@ -18,9 +18,9 @@ export function displayPopupMessage({ modalHeader, modalBody, isError, url }: { } if (isError) { - preview.classList.add("error"); + modal.classList.add("error"); } else { - preview.classList.remove("error"); + modal.classList.remove("error"); } console.trace({ modalHeader, diff --git a/src/home/rendering/render-github-issues.ts b/src/home/rendering/render-github-issues.ts index e98c6a38..0712fc45 100644 --- a/src/home/rendering/render-github-issues.ts +++ b/src/home/rendering/render-github-issues.ts @@ -1,26 +1,25 @@ import { marked } from "marked"; import { organizationImageCache } from "../fetch-github/fetch-issues-full"; -import { TaskMaybeFull } from "../fetch-github/preview-to-full-mapping"; import { GitHubIssue } from "../github-types"; import { taskManager } from "../home"; import { renderErrorInModal } from "./display-popup-modal"; -import { preview, previewBodyInner, titleAnchor, titleHeader } from "./render-preview-modal"; +import { modal, modalBodyInner, titleAnchor, titleHeader } from "./render-preview-modal"; import { setupKeyboardNavigation } from "./setup-keyboard-navigation"; -export function renderGitHubIssues(tasks: TaskMaybeFull[]) { +export function renderGitHubIssues(tasks: GitHubIssue[]) { const container = taskManager.getContainer(); if (container.classList.contains("ready")) { container.classList.remove("ready"); container.innerHTML = ""; } - const existingIssueIds = new Set(Array.from(container.querySelectorAll(".issue-element-inner")).map((element) => element.getAttribute("data-preview-id"))); + const existingIssueIds = new Set(Array.from(container.querySelectorAll(".issue-element-inner")).map((element) => element.getAttribute("data-issue-id"))); let delay = 0; const baseDelay = 1000 / 15; // Base delay in milliseconds for (const task of tasks) { - if (!existingIssueIds.has(task.preview.id.toString())) { - const issueWrapper = everyNewIssue({ taskPreview: task, container }); + if (!existingIssueIds.has(task.id.toString())) { + const issueWrapper = everyNewIssue({ gitHubIssue: task, container }); if (issueWrapper) { setTimeout(() => issueWrapper.classList.add("active"), delay); delay += baseDelay; @@ -32,51 +31,27 @@ export function renderGitHubIssues(tasks: TaskMaybeFull[]) { setupKeyboardNavigation(container); } -function everyNewIssue({ taskPreview, container }: { taskPreview: TaskMaybeFull; container: HTMLDivElement }) { +function everyNewIssue({ gitHubIssue, container }: { gitHubIssue: GitHubIssue; container: HTMLDivElement }) { const issueWrapper = document.createElement("div"); const issueElement = document.createElement("div"); - issueElement.setAttribute("data-preview-id", taskPreview.preview.id.toString()); + issueElement.setAttribute("data-issue-id", gitHubIssue.id.toString()); issueElement.classList.add("issue-element-inner"); - const urlPattern = /https:\/\/github\.com\/([^/]+)\/([^/]+)\//; - if (!taskPreview.preview.body) { - console.warn(`No body found for issue ${taskPreview.preview.id}.`); - return; - } - const match = taskPreview.preview.body.match(urlPattern); - const organizationName = match?.[1]; - - if (!organizationName) { - console.warn(`No organization name found for issue ${taskPreview.preview.id}.`); - return; - } - - const repositoryName = match?.[2]; - if (!repositoryName) { - console.warn("No repository name found"); - return; - } - const labels = parseAndGenerateLabels(taskPreview); - setUpIssueElement(issueElement, taskPreview, organizationName, repositoryName, labels, match); + const labels = parseAndGenerateLabels(gitHubIssue); + const [organizationName, repositoryName] = gitHubIssue.repository_url.split("/").slice(-2); + setUpIssueElement(issueElement, gitHubIssue, organizationName, repositoryName, labels, gitHubIssue.html_url); issueWrapper.appendChild(issueElement); container.appendChild(issueWrapper); return issueWrapper; } -function setUpIssueElement( - issueElement: HTMLDivElement, - task: TaskMaybeFull, - organizationName: string, - repositoryName: string, - labels: string[], - match: RegExpMatchArray | null -) { +function setUpIssueElement(issueElement: HTMLDivElement, task: GitHubIssue, organizationName: string, repositoryName: string, labels: string[], url: string) { const image = ``; issueElement.innerHTML = `

${ - task.preview.title + task.title }

${organizationName}

${repositoryName}

${labels.join( "" )}${image}
`; @@ -95,9 +70,9 @@ function setUpIssueElement( issueWrapper.classList.add("selected"); - const full = task.full; + const full = task; if (!full) { - window.open(match?.input, "_blank"); + window.open(url, "_blank"); } else { previewIssue(task); } @@ -107,12 +82,12 @@ function setUpIssueElement( }); } -function parseAndGenerateLabels(task: TaskMaybeFull) { - type LabelKey = "Pricing: " | "Time: " | "Priority: "; +function parseAndGenerateLabels(task: GitHubIssue) { + type LabelKey = "Price: " | "Time: " | "Priority: "; - const labelOrder: Record = { "Pricing: ": 1, "Time: ": 2, "Priority: ": 3 }; + const labelOrder: Record = { "Price: ": 1, "Time: ": 2, "Priority: ": 3 }; - const { labels, otherLabels } = task.preview.labels.reduce( + const { labels, otherLabels } = task.labels.reduce( (acc, label) => { // check if label is a single string if (typeof label === "string") { @@ -130,7 +105,7 @@ function parseAndGenerateLabels(task: TaskMaybeFull) { }; } - const match = label.name.match(/^(Pricing|Time|Priority): /); + const match = label.name.match(/^(Price|Time|Priority): /); if (match) { const name = label.name.replace(match[0], ""); const labelStr = ``; @@ -144,7 +119,7 @@ function parseAndGenerateLabels(task: TaskMaybeFull) { ); // Sort labels - labels.sort((a, b) => a.order - b.order); + labels.sort((a: { order: number }, b: { order: number }) => a.order - b.order); // Log the other labels if (otherLabels.length) { @@ -156,18 +131,8 @@ function parseAndGenerateLabels(task: TaskMaybeFull) { } // Function to update and show the preview -function previewIssue(taskPreview: TaskMaybeFull) { - const task = taskManager.getTaskByPreviewId(taskPreview.preview.id); - - if (!task) { - throw new Error("Issue not found"); - } - - if (!task.full) { - throw new Error("No full issue found"); - } - - viewIssueDetails(task.full); +function previewIssue(gitHubIssue: GitHubIssue) { + viewIssueDetails(gitHubIssue); } export function viewIssueDetails(full: GitHubIssue) { @@ -175,11 +140,11 @@ export function viewIssueDetails(full: GitHubIssue) { titleHeader.textContent = full.title; titleAnchor.href = full.html_url; if (!full.body) return; - previewBodyInner.innerHTML = marked(full.body) as string; + modalBodyInner.innerHTML = marked(full.body) as string; // Show the preview - preview.classList.add("active"); - preview.classList.remove("error"); + modal.classList.add("active"); + modal.classList.remove("error"); document.body.classList.add("preview-active"); } diff --git a/src/home/rendering/render-preview-modal.ts b/src/home/rendering/render-preview-modal.ts index 7ffad85e..3a48beb9 100644 --- a/src/home/rendering/render-preview-modal.ts +++ b/src/home/rendering/render-preview-modal.ts @@ -1,22 +1,20 @@ -export const preview = document.createElement("div"); -preview.classList.add("preview"); -const previewContent = document.createElement("div"); -previewContent.classList.add("preview-content"); -const previewHeader = document.createElement("div"); -previewHeader.classList.add("preview-header"); +export const modal = document.createElement("div"); +modal.classList.add("preview"); +const modalContent = document.createElement("div"); +modalContent.classList.add("preview-content"); +const modalHeader = document.createElement("div"); +modalHeader.classList.add("preview-header"); export const titleAnchor = document.createElement("a"); titleAnchor.setAttribute("target", "_blank"); -// titleAnchor.href = "#"; export const titleHeader = document.createElement("h1"); const closeButton = document.createElement("button"); closeButton.classList.add("close-preview"); closeButton.innerHTML = ``; -const previewBody = document.createElement("div"); -previewBody.classList.add("preview-body"); -export const previewBodyInner = document.createElement("div"); -previewBodyInner.classList.add("preview-body-inner"); -// Assemble the preview box -previewHeader.appendChild(closeButton); +const modalBody = document.createElement("div"); +modalBody.classList.add("preview-body"); +export const modalBodyInner = document.createElement("div"); +modalBodyInner.classList.add("preview-body-inner"); +modalHeader.appendChild(closeButton); titleAnchor.appendChild(titleHeader); const openNewLinkIcon = ``; const openNewLink = document.createElement("span"); @@ -30,22 +28,22 @@ error.innerHTML = errorIcon; titleAnchor.appendChild(error); titleAnchor.appendChild(openNewLink); -previewHeader.appendChild(titleAnchor); -previewBody.appendChild(previewBodyInner); -previewContent.appendChild(previewHeader); -previewContent.appendChild(previewBody); -preview.appendChild(previewContent); -document.body.appendChild(preview); +modalHeader.appendChild(titleAnchor); +modalBody.appendChild(modalBodyInner); +modalContent.appendChild(modalHeader); +modalContent.appendChild(modalBody); +modal.appendChild(modalContent); +document.body.appendChild(modal); export const issuesContainer = document.getElementById("issues-container"); -closeButton.addEventListener("click", closePreview); +closeButton.addEventListener("click", closeModal); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { - closePreview(); + closeModal(); } }); -function closePreview() { - preview.classList.remove("active"); +function closeModal() { + modal.classList.remove("active"); document.body.classList.remove("preview-active"); } diff --git a/src/home/rendering/setup-keyboard-navigation.ts b/src/home/rendering/setup-keyboard-navigation.ts index f5e0324b..6e04e82a 100644 --- a/src/home/rendering/setup-keyboard-navigation.ts +++ b/src/home/rendering/setup-keyboard-navigation.ts @@ -58,27 +58,25 @@ function keyDownHandler() { container.classList.add("keyboard-selection"); - const previewId = visibleIssues[newIndex].children[0].getAttribute("data-preview-id"); - - const issueElement = visibleIssues.find((issue) => issue.children[0].getAttribute("data-preview-id") === previewId); - - if (issueElement) { - const issueFull = taskManager.getTaskByPreviewId(Number(previewId)).full; - if (issueFull) { - viewIssueDetails(issueFull); + const issueId = visibleIssues[newIndex].children[0].getAttribute("data-issue-id"); + if (issueId) { + const gitHubIssue = taskManager.getGitHubIssueById(parseInt(issueId, 10)); + if (gitHubIssue) { + viewIssueDetails(gitHubIssue); } } } } else if (event.key === "Enter") { const selectedIssue = container.querySelector("#issues-container > div.selected"); if (selectedIssue) { - const previewId = selectedIssue.children[0].getAttribute("data-preview-id"); + const gitHubIssueId = selectedIssue.children[0].getAttribute("data-issue-id"); + if (!gitHubIssueId) { + return; + } - if (previewId) { - const issueFull = taskManager.getTaskByPreviewId(Number(previewId)).full; - if (issueFull) { - window.open(issueFull.html_url, "_blank"); - } + const gitHubIssue = taskManager.getGitHubIssueById(parseInt(gitHubIssueId, 10)); + if (gitHubIssue) { + window.open(gitHubIssue.html_url, "_blank"); } } } else if (event.key === "Escape") { diff --git a/src/home/sorting/sort-issues-by-price.ts b/src/home/sorting/sort-issues-by-price.ts index ddce9167..67951b71 100644 --- a/src/home/sorting/sort-issues-by-price.ts +++ b/src/home/sorting/sort-issues-by-price.ts @@ -1,16 +1,19 @@ -import { TaskMaybeFull } from "../fetch-github/preview-to-full-mapping"; +import { GitHubIssue } from "../github-types"; -export function sortIssuesByPrice(issues: TaskMaybeFull[]) { +export function sortIssuesByPrice(issues: GitHubIssue[]) { return issues.sort((a, b) => { - const aPriceLabel = a.preview.labels.find((label) => label.name.startsWith("Pricing: ")); - const bPriceLabel = b.preview.labels.find((label) => label.name.startsWith("Pricing: ")); - - const aPriceMatch = aPriceLabel ? aPriceLabel.name.match(/Pricing: (\d+)/) : null; - const bPriceMatch = bPriceLabel ? bPriceLabel.name.match(/Pricing: (\d+)/) : null; - - const aPrice = aPriceMatch && aPriceMatch[1] ? parseInt(aPriceMatch[1], 10) : 0; - const bPrice = bPriceMatch && bPriceMatch[1] ? parseInt(bPriceMatch[1], 10) : 0; + const aPrice = a.labels.map(getPriceFromLabel).find((price) => price !== null) ?? -1; + const bPrice = b.labels.map(getPriceFromLabel).find((price) => price !== null) ?? -1; return bPrice - aPrice; }); } + +function getPriceFromLabel(label: string | { name?: string }) { + if (typeof label === "string" || !label.name) return null; + if (label.name.startsWith("Price: ")) { + const match = label.name.match(/Price: (\d+)/); + return match ? parseInt(match[1], 10) : null; + } + return null; +} diff --git a/src/home/sorting/sort-issues-by-priority.ts b/src/home/sorting/sort-issues-by-priority.ts index 3c7b4d22..ba12b740 100644 --- a/src/home/sorting/sort-issues-by-priority.ts +++ b/src/home/sorting/sort-issues-by-priority.ts @@ -1,17 +1,17 @@ -import { TaskMaybeFull } from "../fetch-github/preview-to-full-mapping"; +import { GitHubIssue } from "../github-types"; -export function sortIssuesByPriority(issues: TaskMaybeFull[]) { - return issues.sort((a, b) => { - const priorityRegex = /Priority: (\d+)/; - const aPriorityMatch = a.preview.labels.find((label) => priorityRegex.test(label.name)); - const bPriorityMatch = b.preview.labels.find((label) => priorityRegex.test(label.name)); - - const priorityA = aPriorityMatch ? aPriorityMatch.name.match(priorityRegex) : null; - const priorityB = bPriorityMatch ? bPriorityMatch.name.match(priorityRegex) : null; +export function sortIssuesByPriority(issues: GitHubIssue[]) { + const priorityRegex = /Priority: (\d+)/; - const aPriority = priorityA && priorityA[1] ? parseInt(priorityA[1], 10) : 0; - const bPriority = priorityB && priorityB[1] ? parseInt(priorityB[1], 10) : 0; + return issues.sort((a, b) => { + function getPriority(issue: GitHubIssue) { + const priorityLabel = issue.labels.find( + (label): label is { name: string } => typeof label === "object" && "name" in label && typeof label.name === "string" && priorityRegex.test(label.name) + ); + const match = priorityLabel?.name.match(priorityRegex); + return match ? parseInt(match[1], 10) : -1; + } - return bPriority - aPriority; + return getPriority(b) - getPriority(a); }); } diff --git a/src/home/sorting/sort-issues-by-time.ts b/src/home/sorting/sort-issues-by-time.ts index ad05b72b..cbd08995 100644 --- a/src/home/sorting/sort-issues-by-time.ts +++ b/src/home/sorting/sort-issues-by-time.ts @@ -1,10 +1,10 @@ -import { TaskMaybeFull } from "../fetch-github/preview-to-full-mapping"; +import { GitHubIssue } from "../github-types"; import { calculateTimeLabelValue } from "./calculate-time-label-value"; -export function sortIssuesByTime(tasks: TaskMaybeFull[]) { +export function sortIssuesByTime(tasks: GitHubIssue[]) { return tasks.sort((a, b) => { - const aTimeValue = a.preview.labels.reduce((acc, label) => acc + calculateTimeLabelValue(label.name), 0); - const bTimeValue = b.preview.labels.reduce((acc, label) => acc + calculateTimeLabelValue(label.name), 0); + const aTimeValue = a.labels.reduce((acc, label) => acc + (typeof label === "object" && label?.name ? calculateTimeLabelValue(label.name) : 0), 0); + const bTimeValue = b.labels.reduce((acc, label) => acc + (typeof label === "object" && label?.name ? calculateTimeLabelValue(label.name) : 0), 0); return bTimeValue - aTimeValue; }); } diff --git a/src/home/sorting/sort-issues-by-updated-time.ts b/src/home/sorting/sort-issues-by-updated-time.ts index 1b68af68..e7f40a79 100644 --- a/src/home/sorting/sort-issues-by-updated-time.ts +++ b/src/home/sorting/sort-issues-by-updated-time.ts @@ -1,9 +1,9 @@ -import { TaskMaybeFull } from "../fetch-github/preview-to-full-mapping"; +import { GitHubIssue } from "../github-types"; -export function sortIssuesByLatestActivity(issues: TaskMaybeFull[], ordering: "normal" | "reverse" = "normal") { +export function sortIssuesByLatestActivity(issues: GitHubIssue[], ordering: "normal" | "reverse" = "normal") { return issues.sort((a, b) => { - const dateA = new Date(a.preview.updated_at); - const dateB = new Date(b.preview.updated_at); + const dateA = new Date(a.updated_at); + const dateB = new Date(b.updated_at); return ordering === "normal" ? dateB.getTime() - dateA.getTime() : dateA.getTime() - dateB.getTime(); }); } diff --git a/src/home/sorting/sort-issues-by.ts b/src/home/sorting/sort-issues-by.ts index 43c5fbd6..55b01362 100644 --- a/src/home/sorting/sort-issues-by.ts +++ b/src/home/sorting/sort-issues-by.ts @@ -1,11 +1,11 @@ -import { TaskMaybeFull } from "../fetch-github/preview-to-full-mapping"; +import { GitHubIssue } from "../github-types"; import { SORTING_OPTIONS } from "./generate-sorting-buttons"; import { sortIssuesByPrice } from "./sort-issues-by-price"; import { sortIssuesByPriority } from "./sort-issues-by-priority"; import { sortIssuesByTime } from "./sort-issues-by-time"; import { sortIssuesByLatestActivity } from "./sort-issues-by-updated-time"; -export function sortIssuesBy(tasks: TaskMaybeFull[], sortBy: (typeof SORTING_OPTIONS)[number]) { +export function sortIssuesBy(tasks: GitHubIssue[], sortBy: (typeof SORTING_OPTIONS)[number]) { switch (sortBy) { case "priority": return sortIssuesByPriority(tasks); diff --git a/src/home/sorting/sort-issues-controller.ts b/src/home/sorting/sort-issues-controller.ts index 77cb2ed9..a8633acb 100644 --- a/src/home/sorting/sort-issues-controller.ts +++ b/src/home/sorting/sort-issues-controller.ts @@ -1,10 +1,10 @@ -import { TaskMaybeFull } from "../fetch-github/preview-to-full-mapping"; +import { GitHubIssue } from "../github-types"; import { Sorting } from "./generate-sorting-buttons"; import { sortIssuesBy } from "./sort-issues-by"; import { sortIssuesByPriority } from "./sort-issues-by-priority"; import { sortIssuesByTime } from "./sort-issues-by-time"; -export function sortIssuesController(tasks: TaskMaybeFull[], sorting?: Sorting, options = { ordering: "normal" }) { +export function sortIssuesController(tasks: GitHubIssue[], sorting?: Sorting, options = { ordering: "normal" }) { let sortedIssues = tasks; if (sorting) { diff --git a/src/home/sorting/sorting-manager.ts b/src/home/sorting/sorting-manager.ts index f6c33c17..97d119bc 100644 --- a/src/home/sorting/sorting-manager.ts +++ b/src/home/sorting/sorting-manager.ts @@ -52,12 +52,12 @@ export class SortingManager { const filterText = textBox.value.toLowerCase(); const issues = Array.from(issuesContainer.children) as HTMLDivElement[]; issues.forEach((issue) => { - const issuePreviewId = issue.children[0].getAttribute("data-preview-id"); - if (!issuePreviewId) throw new Error(`No preview id found for issue ${issue}`); - const fullIssue = taskManager.getTaskByPreviewId(Number(issuePreviewId)).full; - if (!fullIssue) throw new Error(`No full issue found for preview id ${issuePreviewId}`); + const issueId = issue.children[0].getAttribute("data-issue-id"); + if (!issueId) return; + const gitHubIssue = taskManager.getGitHubIssueById(parseInt(issueId)); + if (!gitHubIssue) return; const searchableProperties = ["title", "body", "number", "html_url"] as const; - const searchableStrings = searchableProperties.map((prop) => fullIssue[prop]?.toString().toLowerCase()); + const searchableStrings = searchableProperties.map((prop) => gitHubIssue[prop]?.toString().toLowerCase()); const isVisible = searchableStrings.some((str) => str?.includes(filterText)); issue.style.display = isVisible ? "block" : "none"; }); diff --git a/src/home/task-manager.ts b/src/home/task-manager.ts index 59bde855..cb2776b7 100644 --- a/src/home/task-manager.ts +++ b/src/home/task-manager.ts @@ -1,58 +1,39 @@ -import { TaskMaybeFull } from "./fetch-github/preview-to-full-mapping"; import { getGitHubAccessToken } from "./getters/get-github-access-token"; import { setLocalStore } from "./getters/get-local-store"; -import { GITHUB_TASKS_STORAGE_KEY } from "./github-types"; +import { GITHUB_TASKS_STORAGE_KEY, GitHubIssue } from "./github-types"; +import { applyAvatarsToIssues } from "./rendering/render-github-issues"; export class TaskManager { - private _tasks: TaskMaybeFull[] = []; + private _tasks: GitHubIssue[] = []; private _container: HTMLDivElement; constructor(container: HTMLDivElement) { this._container = container; } - public syncTasks(incoming: TaskMaybeFull[]) { - const incomingIds = new Set(incoming.map((task) => task.preview.id)); - const taskMap = new Map(); - - for (const task of incoming) { - const id = task.full?.id || task.preview.id; - taskMap.set(id, task); - } - - for (const task of this._tasks) { - const id = task.full?.id || task.preview.id; - if (!incomingIds.has(id)) { - continue; - } - if (!taskMap.has(id)) { - taskMap.set(id, task); - } else { - const existingTask = taskMap.get(id); - if (existingTask && !existingTask.full && task.full) { - taskMap.set(id, task); - } - } - } - - this._tasks = Array.from(taskMap.values()); + public syncTasks(incoming: GitHubIssue[]) { + this._tasks = incoming; + applyAvatarsToIssues(); + void this._writeToStorage(incoming); } public getTasks() { return this._tasks; } - public getTaskByPreviewId(id: number) { - const task = this._tasks.find((task) => task.preview.id === id); - if (!task) throw new Error(`No task found for preview id ${id}`); - return task; - } - public getContainer() { return this._container; } - public async writeToStorage() { + public getGitHubIssueById(id: number): GitHubIssue | undefined { + return this._tasks.find((task) => task.id === id); + } + + private async _writeToStorage(tasks: GitHubIssue[]) { const _accessToken = await getGitHubAccessToken(); - setLocalStore(GITHUB_TASKS_STORAGE_KEY, { timestamp: Date.now(), tasks: this._tasks, loggedIn: _accessToken !== null }); + setLocalStore(GITHUB_TASKS_STORAGE_KEY, { + timestamp: Date.now(), + tasks: tasks, + loggedIn: _accessToken !== null, + }); } } diff --git a/src/progressive-web-app.ts b/src/progressive-web-app.ts index 04efdf3a..5c8fac82 100644 --- a/src/progressive-web-app.ts +++ b/src/progressive-web-app.ts @@ -1,3 +1,6 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore + self.addEventListener("install", (event: InstallEvent) => { event.waitUntil( caches.open("v1").then((cache) => { @@ -5,7 +8,8 @@ self.addEventListener("install", (event: InstallEvent) => { }) ); }); - +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore self.addEventListener("fetch", (event: FetchEvent) => { event.respondWith( caches.match(event.request).then((response) => { diff --git a/static/index.html b/static/index.html index b60d799b..bda43305 100644 --- a/static/index.html +++ b/static/index.html @@ -55,7 +55,7 @@ d="M132 41.1c0-2.3-1.3-4.5-3.3-5.7L69.4 1.2c-1-.6-2.1-.9-3.3-.9-1.1 0-2.3.3-3.3.9L3.6 35.4c-2 1.2-3.3 3.3-3.3 5.7v68.5c0 2.3 1.3 4.5 3.3 5.7l59.3 34.2c2 1.2 4.5 1.2 6.5 0l59.3-34.2c2-1.2 3.3-3.3 3.3-5.7V41.1zm-11.9 62.5c0 2.7-1.4 5.2-3.7 6.5l-46.6 27.5c-1.1.7-2.4 1-3.7 1s-2.5-.3-3.7-1l-46.6-27.5c-2.3-1.3-3.7-3.8-3.7-6.5V54.1c0-1.2.6-2.4 1.7-3 1.1-.6 2.3-.6 3.4 0l8 4.7c1.9 1.1 3 3.3 4.4 5.8.3.5.5 1 .8 1.4 3.5 6.3 5.2 13 6.8 19.5 3 11.9 6 24.2 21.3 28.2 5 1.3 10.4 1.3 15.4 0 15.2-4 18.3-16.3 21.3-28.2C96.8 76 98.5 69.3 102 63c.3-.5.5-1 .8-1.4 1.3-2.5 2.5-4.6 4.4-5.8l8-4.7c1-.6 2.3-.6 3.4 0s1.7 1.7 1.7 3v49.5zM62.6 13.7c2.2-1.3 4.9-1.3 7.1 0L110 37.6c1 .6 1.6 1 1.6 2.2 0 1.2-.6 1.9-1.6 2.5l-7.7 4.6c-3.4 2-5.1 5.2-6.6 8.1l-.1.2c-.2.4-.4.7-.6 1.1-3.8 6.8-6.6 14-8.2 20.4C83.6 89.1 82.4 97.3 72 100c-1.9.5-3.9.7-5.8.7-2 0-3.9-.3-5.8-.7C50 97.3 48.7 89.1 45.6 76.6 44 70.2 41.2 63 37.4 56.2c-.2-.3-.4-.7-.6-1l-.1-.3c-1.5-2.8-3.3-6.1-6.6-8.1l-7.7-4.6c-1-.6-1.6-1.3-1.6-2.5s.6-1.6 1.6-2.2l40.2-23.8z" >Ubiquity DAO | DevPoolUbiquity DAO | DevPool
diff --git a/tsconfig.json b/tsconfig.json index 01415e27..9091000f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "lib": ["DOM", "ESNext"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */