diff --git a/.cspell.json b/.cspell.json index dfd6093..61ced9c 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,7 +4,21 @@ "ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log"], "useGitignore": true, "language": "en", - "words": ["dataurl", "devpool", "outdir", "servedir"], + "words": [ + "dataurl", + "devpool", + "outdir", + "servedir", + "Supabase", + "supabase", + "SUPABASE", + "Leaderboard", + "leaderboard", + "fract", + "Datas", + "greyscale", + "localstorage" + ], "dictionaries": ["typescript", "node", "software-terms"], "import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"], "ignoreRegExpList": ["[0-9a-fA-F]{6}"] diff --git a/.env.example b/.env.example index e49d79a..7044b33 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ -MY_SECRET="MY_SECRET" +SUPABASE_DB_PASSWORD= +SUPABASE_PROJECT_ID= +SUPABASE_ACCESS_TOKEN= \ No newline at end of file diff --git a/README.md b/README.md index 7b0867a..0cdaa49 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,10 @@ -# `@ubiquity/ts-template` +# `@ubiquity/leaderboard.ubq.fi` -This template repository includes support for the following: +This is an up-to-date leaderboard of the top contributors to the Ubiquity ecosystem based on earnings from completed Devpool Directory bounties. -- TypeScript -- Environment Variables -- Conventional Commits -- Automatic deployment to Cloudflare Pages +### TODO -## Testing - -### Cypress - -To test with Cypress Studio UI, run - -```shell -yarn cy:open -``` - -Otherwise to simply run the tests through the console, run - -```shell -yarn cy:run -``` - -### Jest - -To start Jest tests, run - -```shell -yarn test -``` +- [ ] Pull data from Supabase instead of from static repo file +- [ ] Improve the hunter metadata/markers displayed in the additional details modal +- [ ] Add filters based on markers such as XP, Karma, Top n, etc. +- [ ] Add pagination and cap displayed entries? diff --git a/build/esbuild-build.ts b/build/esbuild-build.ts index 3004b1d..e79535b 100644 --- a/build/esbuild-build.ts +++ b/build/esbuild-build.ts @@ -1,33 +1,78 @@ +import { config } from "dotenv"; import esbuild from "esbuild"; -const typescriptEntries = ["static/main.ts"]; -// const cssEntries = ["static/style.css"]; -const entries = [ - ...typescriptEntries, - // ...cssEntries -]; +import { invertColors } from "./plugins/invert-colors"; +import { execSync } from "child_process"; +config(); + +const typescriptEntries = ["src/home/home.ts"]; +const cssEntries = ["static/style/style.css"]; +const entries = [...typescriptEntries, ...cssEntries, "static/favicon.svg", "static/icon-512x512.png"]; export const esBuildContext: esbuild.BuildOptions = { + plugins: [invertColors], sourcemap: true, entryPoints: entries, bundle: true, minify: false, loader: { - ".png": "dataurl", - ".woff": "dataurl", - ".woff2": "dataurl", - ".eot": "dataurl", - ".ttf": "dataurl", - ".svg": "dataurl", + ".png": "file", + ".woff": "file", + ".woff2": "file", + ".eot": "file", + ".ttf": "file", + ".svg": "file", + ".json": "file", }, outdir: "static/dist", + define: createEnvDefines(["SUPABASE_ACCESS_TOKEN", "SUPABASE_DB_PASSWORD", "SUPABASE_PROJECT_ID"], { + SUPABASE_STORAGE_KEY: generateSupabaseStorageKey(), + commitHash: execSync(`git rev-parse --short HEAD`).toString().trim(), + }), }; esbuild .build(esBuildContext) - .then(() => { - console.log("\tesbuild complete"); - }) - .catch((err) => { - console.error(err); - process.exit(1); - }); + .then(() => console.log("\tesbuild complete")) + .catch(console.error); + +function createEnvDefines(environmentVariables: string[], generatedAtBuild: Record): Record { + const defines: Record = {}; + for (const name of environmentVariables) { + const envVar = process.env[name]; + if (envVar !== undefined) { + defines[name] = JSON.stringify(envVar); + } else { + throw new Error(`Missing environment variable: ${name}`); + } + } + for (const key of Object.keys(generatedAtBuild)) { + if (Object.prototype.hasOwnProperty.call(generatedAtBuild, key)) { + defines[key] = JSON.stringify(generatedAtBuild[key]); + } + } + return defines; +} + +export function generateSupabaseStorageKey(): string | null { + const id = process.env.SUPABASE_PROJECT_ID; + if (!id) { + console.error("SUPABASE_PROJECT_ID environment variable is not set"); + return null; + } + const url = `https://${id}.supabase.co`; + + const urlParts = url.split("."); + if (urlParts.length === 0) { + console.error("Invalid SUPABASE_URL environment variable"); + return null; + } + + const domain = urlParts[0]; + const lastSlashIndex = domain.lastIndexOf("/"); + if (lastSlashIndex === -1) { + console.error("Invalid SUPABASE_URL format"); + return null; + } + + return domain.substring(lastSlashIndex + 1); +} diff --git a/build/plugins/invert-colors.ts b/build/plugins/invert-colors.ts new file mode 100644 index 0000000..a0b3edf --- /dev/null +++ b/build/plugins/invert-colors.ts @@ -0,0 +1,49 @@ +import esbuild from "esbuild"; +import fs from "fs"; +import path from "path"; + +export const invertColors: esbuild.Plugin = { + name: "invert-colors", + setup(build) { + build.onLoad({ filter: /\.css$/ }, async (args) => { + const contents = await fs.promises.readFile(args.path, "utf8"); + + const updatedContents = contents.replace(/prefers-color-scheme: dark/g, "prefers-color-scheme: light"); + + // Invert greyscale colors and accommodate alpha channels in the CSS content + const invertedContents = updatedContents.replace(/#([0-9A-Fa-f]{3,6})([0-9A-Fa-f]{2})?\b/g, (match, rgb, alpha) => { + let color = rgb.startsWith("#") ? rgb.slice(1) : rgb; + if (color.length === 3) { + color = color + .split("") + .map((char: string) => char + char) + .join(""); + } + const r = parseInt(color.slice(0, 2), 16); + const g = parseInt(color.slice(2, 4), 16); + const b = parseInt(color.slice(4, 6), 16); + + // Check if the color is greyscale (R, G, and B components are equal) + if (r === g && g === b) { + // Invert RGB values + const invertedColorValue = (255 - r).toString(16).padStart(2, "0"); + // Return the inverted greyscale color with alpha channel if present + return `#${invertedColorValue}${invertedColorValue}${invertedColorValue}${alpha || ""}`; + } + + // If the color is not greyscale, return it as is, including the alpha channel if present + return `#${color}${alpha || ""}`; + }); + + // Define the output path for the new CSS file + const outputPath = path.resolve("static/style", "inverted-style.css"); + const outputDir = path.dirname(outputPath); + await fs.promises.mkdir(outputDir, { recursive: true }); + // Write the new contents to the output file + await fs.promises.writeFile(outputPath, invertedContents, "utf8"); + + // Return an empty result to esbuild since we're writing the file ourselves + return { contents: "", loader: "css" }; + }); + }, +}; diff --git a/package.json b/package.json index ec1b4ae..9535881 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "ts-template", + "name": "leaderboard-ubq-fi", "version": "1.0.0", - "description": "Template repository with TypeScript support.", + "description": "Active contributor earnings leaderboard for Ubiquity DAO", "main": "build/index.ts", "author": "Ubiquity DAO", "license": "MIT", @@ -30,7 +30,11 @@ "open-source" ], "dependencies": { - "dotenv": "^16.4.4" + "@octokit/rest": "^20.0.2", + "@supabase/supabase-js": "^2.39.0", + "dotenv": "^16.3.1", + "esbuild-plugin-env": "^1.0.8", + "marked": "^11.0.0" }, "devDependencies": { "@commitlint/cli": "^18.6.1", diff --git a/src/home/authentication.ts b/src/home/authentication.ts new file mode 100644 index 0000000..1a263ce --- /dev/null +++ b/src/home/authentication.ts @@ -0,0 +1,17 @@ +import { getGitHubAccessToken } from "./getters/get-github-access-token"; +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"; + +export async function authentication() { + const accessToken = await getGitHubAccessToken(); + if (!accessToken) { + renderGitHubLoginButton(); + } + + const gitHubUser: null | GitHubUser = await getGitHubUser(); + if (gitHubUser) { + displayGitHubUserInformation(gitHubUser); + } +} diff --git a/src/home/getters/get-github-access-token.ts b/src/home/getters/get-github-access-token.ts new file mode 100644 index 0000000..b557105 --- /dev/null +++ b/src/home/getters/get-github-access-token.ts @@ -0,0 +1,78 @@ +import { checkSupabaseSession } from "../rendering/render-github-login-button"; +import { getLocalStore } from "./get-local-store"; +declare const SUPABASE_PROJECT_ID: string; // @DEV: passed in at build time check build/esbuild-build.ts +export async function getGitHubAccessToken(): Promise { + // better to use official function, looking up localstorage has flaws + const authToken = await checkSupabaseSession(); + + const expiresAt = authToken?.expires_at; + if (expiresAt && expiresAt < Date.now() / 1000) { + localStorage.removeItem(`sb-${SUPABASE_PROJECT_ID}-auth-token`); + return null; + } + + return authToken?.provider_token ?? null; +} + +export function getGitHubUserName(): string | null { + const authToken = getLocalStore(`sb-${SUPABASE_PROJECT_ID}-auth-token`) as OauthToken | null; + return authToken?.user?.user_metadata?.user_name ?? null; +} + +export interface OauthToken { + provider_token: string; + access_token: string; + expires_in: number; + expires_at: number; + refresh_token: string; + token_type: string; + user: { + id: string; + aud: string; + role: string; + email: string; + email_confirmed_at: string; + phone: string; + confirmed_at: string; + last_sign_in_at: string; + app_metadata: { provider: string; providers: string[] }; + user_metadata: { + avatar_url: string; + email: string; + email_verified: boolean; + full_name: string; + iss: string; + name: string; + phone_verified: boolean; + preferred_username: string; + provider_id: string; + sub: string; + user_name: string; + }; + identities: [ + { + id: string; + user_id: string; + identity_data: { + avatar_url: string; + email: string; + email_verified: boolean; + full_name: string; + iss: string; + name: string; + phone_verified: boolean; + preferred_username: string; + provider_id: string; + sub: string; + user_name: string; + }; + provider: string; + last_sign_in_at: string; + created_at: string; + updated_at: string; + }, + ]; + created_at: string; + updated_at: string; + }; +} diff --git a/src/home/getters/get-github-user.ts b/src/home/getters/get-github-user.ts new file mode 100644 index 0000000..3cb9256 --- /dev/null +++ b/src/home/getters/get-github-user.ts @@ -0,0 +1,42 @@ +import { Octokit } from "@octokit/rest"; +import { GitHubUser, GitHubUserResponse } from "../github-types"; +import { OauthToken } from "./get-github-access-token"; +import { getLocalStore } from "./get-local-store"; +declare const SUPABASE_STORAGE_KEY: string; // @DEV: passed in at build time check build/esbuild-build.ts + +export async function getGitHubUser(): Promise { + const activeSessionToken = await getSessionToken(); + if (activeSessionToken) { + return getNewGitHubUser(activeSessionToken); + } else { + return null; + } +} + +async function getSessionToken(): Promise { + const cachedSessionToken = getLocalStore(`sb-${SUPABASE_STORAGE_KEY}-auth-token`) as OauthToken | null; + if (cachedSessionToken) { + return cachedSessionToken.provider_token; + } + const newSessionToken = await getNewSessionToken(); + if (newSessionToken) { + return newSessionToken; + } + return null; +} + +async function getNewSessionToken(): Promise { + const hash = window.location.hash; + const params = new URLSearchParams(hash.substr(1)); // remove the '#' and parse + const providerToken = params.get("provider_token"); + if (!providerToken) { + return null; + } + return providerToken; +} + +async function getNewGitHubUser(providerToken: string): Promise { + const octokit = new Octokit({ auth: providerToken }); + const response = (await octokit.request("GET /user")) as GitHubUserResponse; + return response.data; +} diff --git a/src/home/getters/get-indexed-db.ts b/src/home/getters/get-indexed-db.ts new file mode 100644 index 0000000..110ab73 --- /dev/null +++ b/src/home/getters/get-indexed-db.ts @@ -0,0 +1,70 @@ +export async function saveImageToCache({ + dbName, + storeName, + keyName, + orgName, + avatarBlob, +}: { + dbName: string; + storeName: string; + keyName: string; + orgName: string; + avatarBlob: Blob; +}): Promise { + return new Promise((resolve, reject) => { + const open = indexedDB.open(dbName, 2); // Increase version number to ensure onupgradeneeded is called + open.onupgradeneeded = function () { + const db = open.result; + if (!db.objectStoreNames.contains(storeName)) { + db.createObjectStore(storeName, { keyPath: keyName }); + } + }; + open.onsuccess = function () { + const db = open.result; + const transaction = db.transaction(storeName, "readwrite"); + const store = transaction.objectStore(storeName); + const item = { + name: `avatarUrl-${orgName}`, + image: avatarBlob, + created: new Date().getTime(), + }; + store.put(item); + transaction.oncomplete = function () { + db.close(); + resolve(); + }; + transaction.onerror = function (event) { + const errorEventTarget = event.target as IDBRequest; + reject("Error saving image to DB: " + errorEventTarget.error?.message); + }; + }; + }); +} + +export function getImageFromCache({ dbName, storeName, orgName }: { dbName: string; storeName: string; orgName: string }): Promise { + return new Promise((resolve, reject) => { + const open = indexedDB.open(dbName, 2); // Increase version number to ensure onupgradeneeded is called + open.onupgradeneeded = function () { + const db = open.result; + if (!db.objectStoreNames.contains(storeName)) { + db.createObjectStore(storeName, { keyPath: "name" }); + } + }; + open.onsuccess = function () { + const db = open.result; + const transaction = db.transaction(storeName, "readonly"); + const store = transaction.objectStore(storeName); + const getImage = store.get(`avatarUrl-${orgName}`); + getImage.onsuccess = function () { + resolve(getImage.result?.image || null); + }; + transaction.oncomplete = function () { + db.close(); + }; + transaction.onerror = function (event) { + const errorEventTarget = event.target as IDBRequest; + reject("Error retrieving image from DB: " + errorEventTarget.error?.message); + }; + }; + }); +} diff --git a/src/home/getters/get-local-store.ts b/src/home/getters/get-local-store.ts new file mode 100644 index 0000000..ae762f5 --- /dev/null +++ b/src/home/getters/get-local-store.ts @@ -0,0 +1,19 @@ +import { LeaderboardStorage } from "../github-types"; +import { OauthToken } from "./get-github-access-token"; + +export function getLocalStore(key: string): LeaderboardStorage | OauthToken | null { + const cachedIssues = localStorage.getItem(key); + if (cachedIssues) { + try { + return JSON.parse(cachedIssues); // as OauthToken; + } catch (error) { + console.error(error); + } + } + return null; +} + +export function setLocalStore(key: string, value: LeaderboardStorage | OauthToken) { + // remove state from issues before saving to local storage + localStorage[key] = JSON.stringify(value); +} diff --git a/src/home/github-types.ts b/src/home/github-types.ts new file mode 100644 index 0000000..520b834 --- /dev/null +++ b/src/home/github-types.ts @@ -0,0 +1,10 @@ +import { RestEndpointMethodTypes } from "@octokit/plugin-rest-endpoint-methods"; +import { LeaderboardEntry } from "./leaderboard/shared"; + +export const GITHUB_TASKS_STORAGE_KEY = "ubq-leaderboard"; + +export type LeaderboardStorage = Record; + +export type GitHubUserResponse = RestEndpointMethodTypes["users"]["getByUsername"]["response"]; +export type GitHubUser = GitHubUserResponse["data"]; +export type GitHubIssue = RestEndpointMethodTypes["issues"]["get"]["response"]["data"]; diff --git a/src/home/home.ts b/src/home/home.ts new file mode 100644 index 0000000..2696359 --- /dev/null +++ b/src/home/home.ts @@ -0,0 +1,25 @@ +import { grid } from "../the-grid"; +import { authentication } from "./authentication"; +import { readyToolbar } from "./ready-toolbar"; +import { displayLeaderboard } from "./rendering/display-leaderboard"; +import { setupKeyboardNavigation } from "./rendering/setup-keyboard-navigation"; +import { TaskManager } from "./task-manager"; + +grid(document.getElementById("grid") as HTMLElement, () => document.body.classList.add("grid-loaded")); // @DEV: display grid background +const container = document.getElementById("issues-container") as HTMLDivElement; + +if (!container) { + throw new Error("Could not find issues container"); +} +setupKeyboardNavigation(container); + +export const taskManager = new TaskManager(container); +void (async function home() { + try { + void authentication(); + void readyToolbar(); + return await displayLeaderboard(); + } catch (error) { + console.error(error); + } +})(); diff --git a/src/home/leaderboard/data-fetching.ts b/src/home/leaderboard/data-fetching.ts new file mode 100644 index 0000000..19e1495 --- /dev/null +++ b/src/home/leaderboard/data-fetching.ts @@ -0,0 +1,125 @@ +import { Octokit } from "@octokit/rest"; +import { getGitHubAccessToken } from "../getters/get-github-access-token"; +import { getSupabase } from "../rendering/render-github-login-button"; +import { makeLeaderboardEntries } from "./render"; +import { LeaderboardData, SupabaseUser } from "./shared"; + +export async function pullFromSupabase() { + const supabase = getSupabase(); + + // pull all wallets from the database + const { data, error } = await supabase.from("wallets").select("address, id"); + + if (error || !data?.length) { + console.error(error); + return; + } + + const walletMap = new Map(); + + for (const wallet of data) { + walletMap.set(wallet.id, wallet.address); + } + + // pull all users with wallets that are in the walletMap + const users = (await supabase.from("users").select("id, created, wallet_id").in("wallet_id", Array.from(walletMap.keys()))) as { data: SupabaseUser[] }; + + if (!users.data) { + return; + } + + return { walletMap, users }; +} + +export async function fetchAllLeaderboardData() { + const octokit = new Octokit({ auth: await getGitHubAccessToken() }); + const addrAndBalances = await fetchLeaderboardDataFromRepo(); + + const { walletMap, users } = (await pullFromSupabase()) || { walletMap: new Map(), users: { data: [] } }; + + const USER_IDS = users.data.map((user) => user.id); + const githubUsers = await fetchUsernames(USER_IDS, octokit); + const wallets = await makeLeaderboardEntries(walletMap, users, addrAndBalances, githubUsers); + + return wallets.sort((a, b) => b.balance - a.balance); +} + +export async function fetchUsernames(userIds: string[], octokit: Octokit) { + const usernames = []; + + for (const userId of userIds) { + const { data, status } = await octokit.request(`GET /user/${userId}`); + + if (status !== 200) { + console.error(`Failed to fetch user data for ${userId}`); + continue; + } + + usernames.push({ + id: data.id, + username: data.login, + avatar: data.avatar_url, + name: data.name, + }); + } + + return usernames; +} + +export async function fetchLeaderboardDataFromRepo(): Promise { + try { + const token = await getGitHubAccessToken(); + const octokit = new Octokit({ auth: token }); + + // @TODO: create an action that updates this every 24hrs and pulls from a Ubiquity repo + + const path = "leaderboard.csv"; + // cspell: disable + const url = "https://github.com/keyrxng/ubq-airdrop-cli"; + + const { data, status } = await octokit.repos.getContent({ + owner: "keyrxng", + repo: "ubq-airdrop-cli", + path, + }); + + if (status !== 200) { + throw new Error(`Failed to fetch leaderboard data from ${url}`); + } + let parsedData; + + // TODO: remove in place of db pulled stats + if ("content" in data) { + parsedData = atob(data.content); + } else { + throw new Error("No content found in leaderboard data"); + } + + const entries = cvsToLeaderboardData(parsedData); + + if (entries.length === 0) { + throw new Error("No entries found in leaderboard data"); + } + + return entries; + } catch (err) { + console.log(err); + return []; + } +} + +function cvsToLeaderboardData(cvsData: string): { address: string; balance: number }[] { + const lines = cvsData.split("\n"); + const data = []; + for (const line of lines) { + const [address, balance] = line.split(","); + + if (balance === undefined || isNaN(parseInt(balance))) { + continue; + } + + data.push({ address: address.toUpperCase(), balance: parseInt(balance) }); + } + + return data.sort((a, b) => b.balance - a.balance); +} diff --git a/src/home/leaderboard/render.ts b/src/home/leaderboard/render.ts new file mode 100644 index 0000000..fbbe9fa --- /dev/null +++ b/src/home/leaderboard/render.ts @@ -0,0 +1,186 @@ +import { Octokit } from "@octokit/rest"; +import { fetchAllLeaderboardData, fetchLeaderboardDataFromRepo, fetchUsernames, pullFromSupabase } from "./data-fetching"; +import { getGitHubAccessToken } from "../getters/get-github-access-token"; +import { taskManager } from "../home"; +import { preview, previewBodyInner, titleAnchor, titleHeader } from "../rendering/render-preview-modal"; +import { LeaderboardData, LeaderboardEntry, SupabaseUser } from "./shared"; + +export async function renderLeaderboard() { + const container = taskManager.getContainer(); + + const existingAddresses = new Set(Array.from(container.querySelectorAll(".issue-element-inner")).map((element) => element.getAttribute("data-preview-id"))); + + const delay = 0; + const baseDelay = 500 / 15; + + const cachedEntries = localStorage.getItem("ubq-leaderboard") || "[]"; + const lastUpdate = localStorage.getItem("ubq-leaderboard-last-update") || "0"; + const parsedEntries = JSON.parse(cachedEntries) as LeaderboardEntry[]; + + let entries: LeaderboardEntry[] | undefined = []; + let addrAndBalances: LeaderboardData[] = []; + + if (!cachedEntries || Date.now() - parseInt(lastUpdate) > 1000 * 60 * 60 * 24 * 7) { + // fetches the most up to date leaderboard data from the repo + entries = await fetchAllLeaderboardData(); + + if (!entries) { + return; + } + + return launchLeaderboard( + entries.sort((a, b) => b.balance - a.balance), + container, + existingAddresses, + delay, + baseDelay + ); + } else { + if (lastUpdate && Date.now() - parseInt(lastUpdate) < 1000 * 60 * 60 * 24) { + entries = parsedEntries.sort((a, b) => b.balance - a.balance); + + return launchLeaderboard(entries, container, existingAddresses, delay, baseDelay); + } + + addrAndBalances = await fetchLeaderboardDataFromRepo(); + + const { walletMap, users } = (await pullFromSupabase()) || { walletMap: new Map(), users: { data: [] } }; + const USER_IDS = users.data.map((user) => user.id); + const githubUsers = await fetchUsernames(USER_IDS, new Octokit({ auth: await getGitHubAccessToken() })); + + entries = (await makeLeaderboardEntries(walletMap, users, addrAndBalances, githubUsers)).sort((a, b) => b.balance - a.balance); + + if (!entries) { + return; + } + + return launchLeaderboard(entries, container, existingAddresses, delay, baseDelay); + } +} + +async function launchLeaderboard( + entries: LeaderboardEntry[], + container: HTMLDivElement, + existingAddresses: Set, + delay: number, + baseDelay: number +) { + for (const entry of entries) { + if (!existingAddresses.has(entry.address)) { + const entryWrapper = await everyNewEntry({ entry, container }); + if (entryWrapper) { + setTimeout(() => entryWrapper?.classList.add("active"), delay); + delay += baseDelay; + } + } + } + container.classList.add("ready"); + container.setAttribute("data-leaderboard", "true"); + localStorage.setItem("ubq-leaderboard-last-update", Date.now().toString()); + + return true; +} + +async function everyNewEntry({ entry, container }: { entry: LeaderboardEntry; container: HTMLDivElement }) { + const entryWrapper = document.createElement("div"); + const issueElement = document.createElement("div"); + issueElement.setAttribute("data-preview-id", entry.id || ""); + issueElement.classList.add("issue-element-inner"); + + if (!entry.address) { + console.warn("No address found"); + return; + } + + taskManager.addEntry(entry); + + setUpIssueElement(issueElement, entry); + entryWrapper.appendChild(issueElement); + + container.appendChild(entryWrapper); + return entryWrapper; +} + +function setUpIssueElement(entryElement: HTMLDivElement, entry: LeaderboardEntry) { + entryElement.innerHTML = ` +
+
+

${entry.username ?? "Contributor"}

+

$${entry.balance.toLocaleString()}

+
+
+

${entry.address.toUpperCase()}

+
+
+ `; + + entryElement.addEventListener("click", () => { + const entryWrapper = entryElement.parentElement; + + if (!entryWrapper) { + throw new Error("No issue container found"); + } + + Array.from(entryWrapper.parentElement?.children || []).forEach((sibling) => { + sibling.classList.remove("selected"); + }); + + entryWrapper.classList.add("selected"); + + previewEntryAdditionalDetails(entry); + }); +} + +export async function makeLeaderboardEntries( + walletMap: Map, + users: { data: SupabaseUser[] }, + addrAndBalances: LeaderboardData[], + githubUsers: { id: string; username: string }[] +): Promise { + const wallets = users.data.map((user) => { + const wId = Number(user.wallet_id); + const uId = user.id; + + const username = githubUsers.find((user) => user.id === uId)?.username; + + const address = walletMap.get(wId); + + if (!address) { + console.warn(`No address found for wallet ID ${wId}`); + return { address: "", username: "", balance: 0, created_at: "" }; + } + + const balance = addrAndBalances.find((entry) => entry.address.toLowerCase() === address?.toLowerCase())?.balance || 0; + return { + address: address, + username: username, + balance: balance, + created_at: user.created, + id: user.id, + }; + }); + + localStorage.setItem("ubq-leaderboard", JSON.stringify(wallets)); + + return wallets; +} + +export function previewEntryAdditionalDetails(entry: LeaderboardEntry) { + titleHeader.textContent = entry.address; + titleAnchor.href = `https://etherscan.io/address/${entry.address}`; + previewBodyInner.innerHTML = ` +
+
+

${entry.username ?? "Contributor"}

+
+
+ ${entry.created_at ? `

Joined: ${new Date(entry.created_at).toLocaleDateString()}

` : ""} +

Earnings To Date: $${entry.balance.toLocaleString()}

+
+
+ `; + + // Show the preview + preview.classList.add("active"); + document.body.classList.add("preview-active"); +} diff --git a/src/home/leaderboard/shared.ts b/src/home/leaderboard/shared.ts new file mode 100644 index 0000000..960871b --- /dev/null +++ b/src/home/leaderboard/shared.ts @@ -0,0 +1,3 @@ +export type SupabaseUser = { id: string; created: string; wallet_id: string }; +export type LeaderboardData = { address: string; balance: number }; +export type LeaderboardEntry = { address: string; balance: number; username?: string; created_at?: string; id?: string }; diff --git a/src/home/ready-toolbar.ts b/src/home/ready-toolbar.ts new file mode 100644 index 0000000..8bac32f --- /dev/null +++ b/src/home/ready-toolbar.ts @@ -0,0 +1,6 @@ +const toolbar = document.getElementById("toolbar"); +export async function readyToolbar() { + if (!toolbar) throw new Error("toolbar not found"); + toolbar.classList.add("ready"); +} +export { toolbar }; diff --git a/src/home/rendering/display-github-user-information.ts b/src/home/rendering/display-github-user-information.ts new file mode 100644 index 0000000..cb87573 --- /dev/null +++ b/src/home/rendering/display-github-user-information.ts @@ -0,0 +1,34 @@ +import { GitHubUser } from "../github-types"; +import { getSupabase } from "./render-github-login-button"; + +export function displayGitHubUserInformation(gitHubUser: GitHubUser) { + const toolbar = document.getElementById("toolbar"); + const authenticated = document.createElement("div"); + authenticated.id = "authenticated"; + if (!toolbar) throw new Error("toolbar not found"); + + const img = document.createElement("img"); + img.src = gitHubUser.avatar_url; + img.alt = gitHubUser.login; + authenticated.appendChild(img); + + const div = document.createElement("div"); + + div.textContent = gitHubUser.name; + div.classList.add("full"); + authenticated.appendChild(div); + + authenticated.addEventListener("click", async function signOut() { + const supabase = getSupabase(); + const { error } = await supabase.auth.signOut(); + if (error) { + console.error("Error logging out:", error); + alert(error); + } + window.location.reload(); + }); + + toolbar.appendChild(authenticated); + toolbar.setAttribute("data-authenticated", "true"); + toolbar.classList.add("ready"); +} diff --git a/src/home/rendering/display-leaderboard.ts b/src/home/rendering/display-leaderboard.ts new file mode 100644 index 0000000..3624d75 --- /dev/null +++ b/src/home/rendering/display-leaderboard.ts @@ -0,0 +1,15 @@ +import { getGitHubUser } from "../getters/get-github-user"; +import { renderLeaderboard } from "../leaderboard/render"; +import { displayPopupMessage } from "./display-popup-modal"; + +export async function displayLeaderboard() { + if (!(await getGitHubUser())) { + // TODO: remove this after using DB as data source + displayPopupMessage("No GitHub token found", "Please sign in to GitHub to view the leaderboard."); + return; + } + + const killPopup = displayPopupMessage("Fetching leaderboard...", "This may take a couple of moments please wait."); + await renderLeaderboard(); + killPopup(); +} diff --git a/src/home/rendering/display-popup-modal.ts b/src/home/rendering/display-popup-modal.ts new file mode 100644 index 0000000..c9c8581 --- /dev/null +++ b/src/home/rendering/display-popup-modal.ts @@ -0,0 +1,31 @@ +import { toolbar } from "../ready-toolbar"; +import { gitHubLoginButton } from "./render-github-login-button"; +import { preview, previewBodyInner, titleAnchor, titleHeader } from "./render-preview-modal"; + +export function displayPopupMessage(header: string, message: string, url?: string) { + titleHeader.textContent = header; + if (url) { + titleAnchor.href = url; + } + previewBodyInner.innerHTML = message; + + preview.classList.add("active"); + document.body.classList.add("preview-active"); + + if (toolbar) { + toolbar.scrollTo({ + left: toolbar.scrollWidth, + behavior: "smooth", + }); + + gitHubLoginButton?.classList.add("highlight"); + } + + function killPopup() { + preview.classList.remove("active"); + document.body.classList.remove("preview-active"); + gitHubLoginButton?.classList.remove("highlight"); + } + + return killPopup; +} diff --git a/src/home/rendering/render-github-login-button.ts b/src/home/rendering/render-github-login-button.ts new file mode 100644 index 0000000..50cb9b0 --- /dev/null +++ b/src/home/rendering/render-github-login-button.ts @@ -0,0 +1,43 @@ +import { createClient } from "@supabase/supabase-js"; +import { toolbar } from "../ready-toolbar"; + +declare const SUPABASE_PROJECT_ID: string; // @DEV: passed in at build time check build/esbuild-build.ts +declare const SUPABASE_DB_PASSWORD: string; // @DEV: passed in at build time check build/esbuild-build.ts + +const SUPABASE_URL = `https://${SUPABASE_PROJECT_ID}.supabase.co`; +const supabase = createClient(SUPABASE_URL, SUPABASE_DB_PASSWORD); + +export function getSupabase() { + return supabase; +} + +export async function checkSupabaseSession() { + const { + data: { session }, + } = await supabase.auth.getSession(); + + return session; +} + +async function gitHubLoginButtonHandler() { + const { error } = await supabase.auth.signInWithOAuth({ + provider: "github", + options: { + scopes: "repo", + }, + }); + if (error) { + console.error("Error logging in:", error); + } +} +const gitHubLoginButton = document.createElement("button"); +export function renderGitHubLoginButton() { + gitHubLoginButton.id = "github-login-button"; + gitHubLoginButton.innerHTML = "Login With GitHub"; + gitHubLoginButton.addEventListener("click", gitHubLoginButtonHandler); + if (toolbar) { + toolbar.appendChild(gitHubLoginButton); + toolbar.classList.add("ready"); + } +} +export { gitHubLoginButton }; diff --git a/src/home/rendering/render-preview-modal.ts b/src/home/rendering/render-preview-modal.ts new file mode 100644 index 0000000..af5d46d --- /dev/null +++ b/src/home/rendering/render-preview-modal.ts @@ -0,0 +1,44 @@ +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 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); +titleAnchor.appendChild(titleHeader); +const openNewLinkIcon = ``; +const openNewLink = document.createElement("span"); +openNewLink.classList.add("open-new-link"); +openNewLink.innerHTML = openNewLinkIcon; +titleAnchor.appendChild(openNewLink); +previewHeader.appendChild(titleAnchor); +previewBody.appendChild(previewBodyInner); +previewContent.appendChild(previewHeader); +previewContent.appendChild(previewBody); +preview.appendChild(previewContent); +document.body.appendChild(preview); +export const issuesContainer = document.getElementById("issues-container"); + +closeButton.addEventListener("click", closePreview); +document.addEventListener("keydown", (event) => { + if (event.key === "Escape") { + closePreview(); + } +}); + +function closePreview() { + preview.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 new file mode 100644 index 0000000..b7daf82 --- /dev/null +++ b/src/home/rendering/setup-keyboard-navigation.ts @@ -0,0 +1,89 @@ +import { taskManager } from "../home"; +import { previewEntryAdditionalDetails } from "../leaderboard/render"; + +const DATA_PREVIEW_ID = "data-preview-id"; + +const keyDownHandlerCurried = keyDownHandler(); +const disableKeyBoardNavigationCurried = disableKeyboardNavigationCurry; + +let isKeyDownListenerAdded = false; +let isMouseOverListenerAdded = false; + +export function setupKeyboardNavigation(container: HTMLDivElement) { + if (!isKeyDownListenerAdded) { + document.addEventListener("keydown", keyDownHandlerCurried); + isKeyDownListenerAdded = true; + } + if (!isMouseOverListenerAdded) { + container.addEventListener("mouseover", disableKeyBoardNavigationCurried); + isMouseOverListenerAdded = true; + } +} + +function disableKeyboardNavigationCurry() { + const container = document.getElementById("issues-container") as HTMLDivElement; + return disableKeyboardNavigation(container); +} + +function disableKeyboardNavigation(container: HTMLDivElement) { + container.classList.remove("keyboard-selection"); +} + +function keyDownHandler() { + const container = document.getElementById("issues-container") as HTMLDivElement; + return function keyDownHandler(event: KeyboardEvent) { + if (event.key === "ArrowUp" || event.key === "ArrowDown") { + verticalKeys(event, container); + } else if (event.key === "Enter") { + const selectedIssue = container.querySelector("#issues-container > div.selected"); + const previewId = selectedIssue?.children[0].getAttribute(DATA_PREVIEW_ID); + const issueFull = taskManager.getEntryById(Number(previewId)); + + previewEntryAdditionalDetails(issueFull); + } else if (event.key === "Escape") { + disableKeyboardNavigation(container); + } + }; +} + +function verticalKeys(event: KeyboardEvent, container: HTMLDivElement) { + const issues = Array.from(container.children) as HTMLElement[]; + const visibleIssues = issues.filter((issue) => issue.style.display !== "none"); + const activeIndex = visibleIssues.findIndex((issue) => issue.classList.contains("selected")); + const originalIndex = activeIndex === -1 ? -1 : activeIndex; + let newIndex = originalIndex; + + if (event.key === "ArrowUp" && originalIndex > 0) { + newIndex = originalIndex - 1; + event.preventDefault(); + } else if (event.key === "ArrowDown" && originalIndex < visibleIssues.length - 1) { + newIndex = originalIndex + 1; + event.preventDefault(); + } + + if (newIndex !== originalIndex) { + visibleIssues.forEach((issue) => { + issue.classList.remove("selected"); + }); + + visibleIssues[newIndex]?.classList.add("selected"); + visibleIssues[newIndex].scrollIntoView({ + behavior: "smooth", + block: "center", + }); + + 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) { + throw new Error("No issue found"); + } + + const issueFull = taskManager.getEntryById(Number(previewId)); + + previewEntryAdditionalDetails(issueFull); + } +} diff --git a/src/home/task-manager.ts b/src/home/task-manager.ts new file mode 100644 index 0000000..c44a638 --- /dev/null +++ b/src/home/task-manager.ts @@ -0,0 +1,28 @@ +import { setLocalStore } from "./getters/get-local-store"; +import { GITHUB_TASKS_STORAGE_KEY } from "./github-types"; +import { LeaderboardEntry } from "./leaderboard/shared"; + +export class TaskManager { + private _entries: Record = {}; + private _container: HTMLDivElement; + + constructor(container: HTMLDivElement) { + this._container = container; + } + + public addEntry(entry: LeaderboardEntry) { + if (!entry.id) throw new Error("Entry must have an id"); + this._entries[entry.id] = entry; + } + + public getContainer() { + return this._container; + } + + getEntryById(position: number) { + return this._entries[position]; + } + public async writeToStorage() { + setLocalStore(GITHUB_TASKS_STORAGE_KEY, this._entries); + } +} diff --git a/src/the-grid.ts b/src/the-grid.ts new file mode 100644 index 0000000..02c651f --- /dev/null +++ b/src/the-grid.ts @@ -0,0 +1,162 @@ +export function grid(node = document.body, callback?: () => void) { + // Create canvas and WebGL context + const canvas = document.createElement("canvas"); + const devicePixelRatio = window.devicePixelRatio || 1; + canvas.width = window.innerWidth * devicePixelRatio; + canvas.height = window.innerHeight * devicePixelRatio; + node.appendChild(canvas); + + const gl = canvas.getContext("webgl") as WebGLRenderingContext; + + // Enable alpha blending + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + // Define shader sources + const vertexShaderSource = ` + attribute vec2 a_position; + + void main() { + gl_Position = vec4(a_position, 0, 1); + } +`; + + // cspell:ignore mediump + const fragmentShaderSource = ` + precision mediump float; + + uniform vec2 u_resolution; + uniform float u_time; + + float rand(vec2 n) { + return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453); + } + + void main() { + vec3 color = vec3(128.0/255.0, 128.0/255.0, 128.0/255.0); // #808080 + vec2 tilePosition = mod(gl_FragCoord.xy, 24.0); + vec2 tileNumber = floor(gl_FragCoord.xy / 24.0); + + float period = rand(tileNumber) * 9.0 + 1.0; // Random value in the range [1, 10] + float phase = fract(u_time / period / 8.0); // Animation eight times slower + float opacity = (1.0 - abs(phase * 2.0 - 1.0)) * 0.125; // Limit maximum opacity to 0.25 + + vec4 backgroundColor = vec4(color, opacity); + + if (tilePosition.x > 23.0 && tilePosition.y < 1.0) { + gl_FragColor = vec4(color, 1.0); // Full opacity for the dot + } else { + gl_FragColor = backgroundColor; + } + } +`; + + // Define shader creation function + function createShader(gl: WebGLRenderingContext, type: number, source: string) { + const shader = gl.createShader(type); + if (!shader) { + console.error("An error occurred creating the shaders"); + return null; + } + gl.shaderSource(shader, source); + gl.compileShader(shader); + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + console.error("An error occurred compiling the shaders: " + gl.getShaderInfoLog(shader)); + gl.deleteShader(shader); + return null; + } + return shader; + } + + // Create vertex and fragment shaders + const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource); + if (!vertexShader) { + console.error("An error occurred creating the vertex shader"); + return; + } + const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource); + if (!fragmentShader) { + console.error("An error occurred creating the fragment shader"); + return; + } + + // Create program, attach shaders, and link + const program = gl.createProgram(); + if (!program) { + console.error("An error occurred creating the program"); + return; + } + + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + // Verify program link status + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.error("Unable to initialize the shader program: " + gl.getProgramInfoLog(program)); + return; + } + + // Use the program + gl.useProgram(program); + + // Get location of time and resolution uniforms + const timeUniformLocation = gl.getUniformLocation(program, "u_time"); + const resolutionUniformLocation = gl.getUniformLocation(program, "u_resolution"); + + // Bind the position buffer and set attribute pointer + const positionBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW); + + const positionAttributeLocation = gl.getAttribLocation(program, "a_position"); + gl.enableVertexAttribArray(positionAttributeLocation); + gl.vertexAttribPointer(positionAttributeLocation, 2, gl.FLOAT, false, 0, 0); + + // Resize function + function resizeCanvasToDisplaySize(canvas: HTMLCanvasElement) { + // Lookup the size the browser is displaying the canvas. + const displayWidth = window.innerWidth; + const displayHeight = window.innerHeight; + + // Check if the canvas is not the same size. + if (canvas.width != displayWidth || canvas.height != displayHeight) { + // Make the canvas the same size + canvas.width = displayWidth; + canvas.height = displayHeight; + + // Update WebGL viewport to match + gl.viewport(0, 0, canvas.width, canvas.height); + } + } + + // Render function + function render() { + resizeCanvasToDisplaySize(canvas); // Check and update canvas size each frame + + // Update resolution uniform + gl.uniform2f(resolutionUniformLocation, canvas.width, canvas.height); + + gl.clearColor(0.0, 0.0, 0.0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + + // Update time uniform + gl.uniform1f(timeUniformLocation, performance.now() / 1000.0); + + // Draw + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + + // Request next frame + requestAnimationFrame(render); + } + + // Handle window resize + window.addEventListener("resize", () => resizeCanvasToDisplaySize(canvas)); + + // Callback + if (callback) { + callback(); + } + // Start the render loop + render(); +} diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..75322cc --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/static/icon-512x512.png b/static/icon-512x512.png new file mode 100644 index 0000000..7ebb3ae Binary files /dev/null and b/static/icon-512x512.png differ diff --git a/static/index.html b/static/index.html index dbae5f4..23fb038 100644 --- a/static/index.html +++ b/static/index.html @@ -2,12 +2,57 @@ - - Ubiquity TypeScript Template - + Leaderboard | Ubiquity DAO + + + + + + + + + + + + + + + + + + + + + + + + + + + -

Ubiquity TypeScript Template

- +
+
+
+
+ + + Ubiquity DAO | + Leaderboard + +
+
+ + diff --git a/static/main.ts b/static/main.ts deleted file mode 100644 index b19bfa7..0000000 --- a/static/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -export async function mainModule() { - console.log(`Hello from mainModule`); -} -mainModule() - .then(() => { - console.log("mainModule loaded"); - }) - .catch((error) => { - console.error(error); - }); diff --git a/static/style/fonts/ubiquity-nova-standard.eot b/static/style/fonts/ubiquity-nova-standard.eot new file mode 100644 index 0000000..1e46099 Binary files /dev/null and b/static/style/fonts/ubiquity-nova-standard.eot differ diff --git a/static/style/fonts/ubiquity-nova-standard.ttf b/static/style/fonts/ubiquity-nova-standard.ttf new file mode 100644 index 0000000..d953486 Binary files /dev/null and b/static/style/fonts/ubiquity-nova-standard.ttf differ diff --git a/static/style/fonts/ubiquity-nova-standard.woff b/static/style/fonts/ubiquity-nova-standard.woff new file mode 100644 index 0000000..0c2691e Binary files /dev/null and b/static/style/fonts/ubiquity-nova-standard.woff differ diff --git a/static/style/inverted-style.css b/static/style/inverted-style.css new file mode 100644 index 0000000..b80c26e --- /dev/null +++ b/static/style/inverted-style.css @@ -0,0 +1,702 @@ +@media (prefers-color-scheme: light) { + :root { + --grid-background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABYWlDQ1BrQ0dDb2xvclNwYWNlRGlzcGxheVAzAAAokWNgYFJJLCjIYWFgYMjNKykKcndSiIiMUmB/yMAOhLwMYgwKicnFBY4BAT5AJQwwGhV8u8bACKIv64LMOiU1tUm1XsDXYqbw1YuvRJsw1aMArpTU4mQg/QeIU5MLikoYGBhTgGzl8pICELsDyBYpAjoKyJ4DYqdD2BtA7CQI+whYTUiQM5B9A8hWSM5IBJrB+API1klCEk9HYkPtBQFul8zigpzESoUAYwKuJQOUpFaUgGjn/ILKosz0jBIFR2AopSp45iXr6SgYGRiaMzCAwhyi+nMgOCwZxc4gxJrvMzDY7v////9uhJjXfgaGjUCdXDsRYhoWDAyC3AwMJ3YWJBYlgoWYgZgpLY2B4dNyBgbeSAYG4QtAPdHFacZGYHlGHicGBtZ7//9/VmNgYJ/MwPB3wv//vxf9//93MVDzHQaGA3kAFSFl7jXH0fsAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAACNJREFUSA3t0IEAAAAMBKFHm7/UTaQQWnXDgAEDBgwYMGDgAXaJAz4RVVHYAAAAAElFTkSuQmCC"); + --background-color-default-brightness: 2%; + --background-color-light-brightness: 6%; + --border-brightness: 5%; + --background-color-default: hsl(225 50% var(--background-color-default-brightness) / 1); + --background-color-light: hsl(225 50% var(--background-color-light-brightness) / 1); + --border-color: hsl(225 25% var(--border-brightness) / 1); + } + #logo { + height: 28px; + margin-right: 4px; + } + #authenticated, + #branding { + opacity: 0.5; + transition: 125ms opacity ease-in-out; + } + #authenticated:hover, + #branding:hover { + opacity: 1; + } + #branding > span { + padding: 8px; + } + body, + html { + margin: 0; + padding: 0; + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; + } + * { + font-family: "Proxima Nova", "Ubiquity Nova", sans-serif; + color: #000000; + font-weight: 500; + } + @font-face { + font-family: "Ubiquity Nova"; + font-style: normal; + font-weight: 400; + src: url(fonts/ubiquity-nova-standard.eot); + src: + url(fonts/ubiquity-nova-standard.eot#iefix) format("embedded-opentype"), + url(fonts/ubiquity-nova-standard.woff) format("woff"), + url(fonts/ubiquity-nova-standard.ttf) format("truetype"); + } + #issues-container { + max-width: 640px; + scrollbar-width: none; + -ms-overflow-style: none; + padding: 56px 0; + transition: 0.25s all cubic-bezier(0, 1, 1, 1); + } + body.preview-active #issues-container { + transform: translateX(-50%); + } + &::-webkit-scrollbar { + display: none; + } + #issues-container .issue-element-inner { + transition: 125ms opacity ease-out 62.5ms; + } + #issues-container:hover .issue-element-inner { + opacity: 0.5; + } + #issues-container.keyboard-selection:hover .issue-element-inner { + opacity: 1; + } + #issues-container * { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + #issues-container > div:first-child { + border-top: 1px solid #7f7f7f20; + } + #issues-container > div:last-child { + border-top: 1px solid #7f7f7f20; + } + #issues-container > div { + padding: 0 16px; + overflow: hidden; + background-size: 56px; + background-position: 0; + background-repeat: no-repeat; + opacity: 0.125; + margin: 3px auto; + border: 1px solid #7f7f7f20; + border-radius: 4px; + cursor: pointer; + margin: 4px; + filter: blur(4px); + display: block; + /* border-color: var(--border-color); */ + /* box-shadow: inset 0 0 24px #00bfff10; */ + } + #issues-container > div.selected, + #issues-container > div.selected .info, + #issues-container > div.selected h3 { + opacity: 1; + } + #issues-container.keyboard-selection > div.active { + opacity: 0.33; + } + #issues-container.keyboard-selection > div.active.selected { + opacity: 1; + } + #issues-container > div.active { + transition: 125ms all ease-in-out; + opacity: 1; + filter: blur(0); + } + #issues-container > div:hover { + opacity: 1; + transition: background-color 0s; + } + #issues-container > div:hover .info { + opacity: 1; + } + #issues-container > div:hover .issue-element-inner { + transition: 125ms opacity ease-in-out; + opacity: 1; + } + #issues-container > div:active { + background-color: #7f7f7f40; + } + #issues-container h3 { + line-height: 1; + font-size: 16px; + text-overflow: ellipsis; + margin: 0; + } + #issues-container p { + text-align: right; + } + p { + margin: 0; + line-height: 1; + color: #404040; + font-size: 12px; + letter-spacing: 0.5px; + text-rendering: geometricPrecision; + top: 0; + right: 0; + } + li { + margin: 0; + line-height: 1; + color: #404040; + font-size: 12px; + letter-spacing: 0.5px; + text-rendering: geometricPrecision; + top: 0; + right: 0; + } + .issue-element-inner { + position: relative; + text-align: left; + display: flex; + padding: 12px 0; + align-items: center; + } + p.organization-name { + opacity: 0.5; + display: inline-block; + } + p.repository-name { + display: inline-block; + } + label:first-child { + margin-left: 0; + } + label { + padding: 4px 6px; + border-radius: 4px; + margin: 0 4px 0; + font-size: 12px; + text-align: center; + white-space: nowrap; + background-color: #7f7f7f20; + width: 64px; + letter-spacing: 0.5px; + flex-grow: 4; + justify-content: center; + display: flex; + align-items: center; + height: 16px; + text-overflow: ellipsis; + overflow: hidden; + display: inline-block; + } + input[type="radio"] { + background-color: unset; + cursor: default; + appearance: unset; + display: none; + box-sizing: border-box; + margin: 0; + padding: 0; + border: none; + } + #filters label { + cursor: pointer; + min-width: fit-content; + } + #filters label:hover { + background-color: #7f7f7f40; + } + #filters label:active { + background-color: #7f7f7f80; + } + input[type="radio"]:checked + label { + background-color: #7f7f7f80 !important; + } + .labels { + display: flex; + margin-left: auto; + } + .partner { + bottom: 0; + margin: 0; + } + body { + display: flex; + align-items: center; + align-content: center; + justify-content: center; + } + .info { + opacity: 0.66; + transition: 125ms opacity ease-in-out; + align-items: center; + } + #issues-container > .issue-element-wrapper { + opacity: 0.5; + transition: 0.33s all cubic-bezier(0, 1, 1, 1); + filter: blur(8px); + } + button { + appearance: unset; + text-rendering: unset; + color: unset; + letter-spacing: unset; + word-spacing: unset; + line-height: unset; + text-transform: unset; + text-indent: unset; + text-shadow: unset; + display: unset; + text-align: unset; + align-items: unset; + cursor: pointer; + box-sizing: unset; + background-color: unset; + margin: unset; + padding-block: unset; + padding-inline: unset; + border-width: unset; + border-style: unset; + border-color: unset; + border-image: unset; + border: 1px solid #7f7f7f20; + padding: 8px 16px; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 1px; + background-color: #7f7f7f10; + } + button:hover { + background-color: #7f7f7f20; + } + button:active { + background-color: #7f7f7f40; + } + .toolbar { + position: fixed; + width: calc(100vw - 8px); + height: 48px; + top: 0; + backdrop-filter: blur(8px); + padding: 4px; + display: flex; + border-bottom: 1px solid #7f7f7f20; + justify-content: center; + -webkit-backdrop-filter: blur(8px); + justify-content: space-between; + justify-content: space-evenly; + } + .toolbar > * { + align-items: center; + display: inline-flex; + text-align: left; + margin: 0 16px; + } + #authenticated > * { + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0 4px; + max-height: 48px; + } + #authenticated > img { + border-radius: 50%; + height: 40px; + padding: 4px 0; + } + .toolbar[data-authenticated="true"] > #github-login-button { + display: none; + } + .toolbar { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + opacity: 0; + transition: 0.5s opacity ease-in-out; + } + .toolbar.ready { + opacity: 1; + } + #branding { + text-transform: uppercase; + letter-spacing: 1.5px; + font-weight: 400; + text-rendering: geometricPrecision; + } + .full { + display: unset !important; + } + @media screen and (max-width: 804px) { + .mid { + display: none !important; + } + #branding > span { + padding: 0; + } + } + @media screen and (max-width: 1139px) { + .full { + display: none !important; + } + .issue-element-inner { + display: block; + } + #issues-container .labels { + margin-top: 4px; + } + #branding { + margin: auto; + margin-left: 12px; + } + .toolbar { + overflow-x: auto; + margin: 0; + padding-left: 0; + display: flex; + align-items: center; + justify-content: center; + } + .toolbar > * { + margin-left: 12px; + } + .toolbar button { + /* padding: 12px 16px; */ + height: 24px !important; + padding: 4px 16px; + line-height: 1; + } + } + @media screen and (max-width: 1280px) { + body.preview-active { + overflow: hidden; + } + .preview.active { + transform: translateX(0) !important; + } + body.preview-active #issues-container { + transform: translateX(0) !important; + } + body.preview-active > #issues-container > div, + body.preview-active > background { + filter: blur(8px); + pointer-events: none; + } + } + .toolbar > button { + text-align: center; + } + #filters button { + margin-top: 8px; + margin-right: 8px; + } + #issues-container img { + height: 24px; + border-radius: 24px; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + opacity: 0; + transition: 0.25s opacity ease-in-out; + } + #issues-container img[src] { + opacity: 1; + } + + background, + background #grid { + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + } + background #grid { + /* -webkit-mask-image: radial-gradient(#ffffff20 0, #ffffff80 100%); */ + /* mask-image: radial-gradient(#ffffff20 0, #ffffff80 100%); */ + pointer-events: none; + } + .preview blockquote { + margin-left: 16px; + opacity: 0.66; + } + .preview sup { + display: block; + } + .preview hr { + border-color: #00000040; + } + .preview p { + line-height: 1.25; + word-break: break-word; + margin-bottom: 12px; + letter-spacing: 0; + font-size: 16px; + } + .preview img { + max-width: 100%; + } + .preview pre { + position: relative; + display: inline-block; + width: 100%; + overflow: scroll; + height: 48px; + } + .preview pre code { + position: absolute; + font-family: monospace; + } + .preview ol, + .preview ul { + padding-left: 12px; + margin-left: 4px; + } + .preview h1 { + margin: 8px; + font-size: 24px; + margin-block-start: 0; + margin-block-end: 0; + margin-inline: 0; + margin-inline-end: 0; + } + .preview a { + word-break: break-all; + } + .preview a[href*="//"] .open-new-link svg + { + fill: #00000080; + vertical-align: middle; + height: 20px; + } + .preview-body-inner { + line-height: 1.25; + } + .preview-body-inner > :last-child { + margin-bottom: 0; + } + .preview { + text-align: left; + position: fixed; + transition: 0.25s all cubic-bezier(0, 1, 1, 1); + max-width: 640px; + border: 1px solid #7f7f7f20; + border-radius: 4px; + opacity: 0; + top: 60px; + /* flex-direction: row; */ + /* display: flex; */ + /* transform: translateX(0); */ + /* flex-wrap: wrap; */ + pointer-events: none; + /* overflow: hidden; */ + width: calc(100vw - 14px); + } + .preview * { + pointer-events: none; + } + .preview.active { + pointer-events: all; + } + .preview.active * { + pointer-events: all; + } + .close-preview svg { + fill: #00000080; + vertical-align: middle; + height: 36px; + } + .preview.active { + opacity: 1; + pointer-events: all; + transform: translateX(50%); + /* box-shadow: 0 0px 16px #00000010; */ + } + .preview-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; + } + .preview-header > a { + word-wrap: initial; + text-decoration: none; + opacity: 0.75; + display: flex; + justify-content: right; + align-items: center; + width: calc(100% - 72px); + } + .preview-header > a:hover { + opacity: 1; + } + .preview-header > a > h1 { + word-wrap: initial; + word-break: normal; + display: inline; + vertical-align: middle; + margin-right: 12px; + text-align: right; + font-size: 20px; + text-align: right; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .preview-body { + overflow: scroll; + margin: 0; + border-top: 1px solid #dfdfdf; + padding: 16px; + max-height: calc(100vh - (80px)); + max-height: calc(100vh - (242px)); + /* border-color: var(--border-color); */ + } + .preview-content { + width: 100%; + } + .preview button.close-preview { + right: 0; + top: 0; + margin-right: 8px; + padding: 8px 8px; + border: none; + background: 0 0; + padding: 0; + } + .preview button.close-preview:hover svg { + fill: #000000; + } + .preview li { + margin-bottom: 8px; + letter-spacing: 0; + font-size: 16px; + line-height: 1.25; + } + #filters input[type="text"] { + margin: 0 4px; + padding: 4px; + border: none; + border-radius: 4px; + background-color: #7f7f7f20; + width: 100%; + height: 16px; + text-align: center; + min-width: 80px; + } + #filters input[type="text"]::placeholder { + font-size: 12px; + letter-spacing: 0.5px; + } + #filters input[type="text"].hidden { + display: none; + } + button#github-login-button { + margin-left: 0; + } + + p.organization-name::after { + content: "/"; + } + #loader { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: #ffffff80; + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + } + + input[type="radio"][data-ordering="reverse"]:not(#leaderboard) + label::after { + margin-left: 6px; + content: "▲"; + } + + input[type="radio"][data-ordering="normal"]:not(#leaderboard) + label::after { + margin-left: 6px; + content: "▼"; + } + + #bottom-bar { + top: unset; + bottom: 0; + opacity: 1; + border-top: 1px solid #7f7f7f20; + border-bottom: unset; + } + #bottom-bar a { + text-decoration: none; + } + #bottom-bar a button { + height: 30px; + } + .toolbar button { + white-space: nowrap; + } + + .info .entry-title { + font-size: 16px; + line-height: 1.25; + margin: 0; + margin-bottom: 4px; + display: grid; + grid-template-columns: 1fr 1fr; + } + + .info .entry-body { + width: 100%; + font-size: 12px; + line-height: 1.25; + margin: 0; + margin-bottom: 4px; + display: grid; + grid-template-columns: 1fr 1fr; + } + + background #grid { + pointer-events: none; + } + + background #grid canvas { + width: 100%; + height: 100%; + opacity: 0; + animation: background-grid-fade-in 3s ease-in-out forwards; + } + + background .gradient { + width: 200vw; + height: 200vh; + position: absolute; + opacity: 0; + } + .grid-loaded background .gradient { + background-image: radial-gradient(#00bfff00 0%, #00bfffff 15%, #00bfff00 34%, #00bfffff 58%, #00bfff00 75%, #00bfffff 100%); + animation: background-gradients-fade-in 3s ease-in-out forwards; + } + background > :nth-child(1) { + transform: translateX(-100vw); + } + background > :nth-child(2) { + transform: translateY(-50vh); + } + + @keyframes background-gradients-fade-in { + to { + opacity: 0.125; + } + } + @keyframes background-grid-fade-in { + to { + opacity: 0.5; + } + } +} diff --git a/static/style/special.css b/static/style/special.css new file mode 100644 index 0000000..cacf08d --- /dev/null +++ b/static/style/special.css @@ -0,0 +1,86 @@ +:root { + --dark-background: #000210; + --dark-background-half: #00021080; + --light-background: #f8f8f8; + --light-background-half: #f8f8f880; +} + +@media (prefers-color-scheme: dark) { + background { + background-color: var(--dark-background); + } + html, + #issues-container > div, + .preview { + background-color: var(--dark-background); + border-color: var(--border-color); + border-color: #80808020; + } + #issues-container > div, + .preview > .preview-content { + box-shadow: inset 0 0 24px #0080ff08; + } + + .toolbar.ready { + background-color: var(--dark-background-half); + } + body.preview-active button#github-login-button.highlight { + /* background-color: #008080; */ + /* border-width: 0; */ + animation: highlight-dark-mode 1s ease-in-out infinite alternate; + } + .grid-loaded #bottom-bar { + background-color: var(--dark-background-half); + } +} + +@media (prefers-color-scheme: light) { + #issues-container > div, + .preview > .preview-content { + box-shadow: inset 0 0 24px #00000008; + } + background { + /* background-color: var(--light-background); */ + } + #grid { + /* opacity: 0.25; */ + filter: invert(1); + } + #issues-container > div, + .preview { + background-color: var(--light-background); + } + .toolbar.ready { + background-color: var(--light-background-half); + } + .grid-loaded #bottom-bar { + background-color: var(--light-background-half); + } + body.preview-active button#github-login-button.highlight { + animation: highlight-light-mode 1s ease-in-out infinite alternate; + } + .grid-loaded background .gradient { + opacity: 0; + animation: none; + background-image: none; + } +} + +@keyframes highlight-dark-mode { + from { + background-color: #808080; + box-shadow: 0 0 24px 0px #808080; + } + to { + background-color: #000; + } +} +@keyframes highlight-light-mode { + from { + background-color: #bfbfbf; + } + to { + background-color: #fff; + box-shadow: 0 0 24px 12px #fff; + } +} diff --git a/static/style/style.css b/static/style/style.css new file mode 100644 index 0000000..41029b1 --- /dev/null +++ b/static/style/style.css @@ -0,0 +1,702 @@ +@media (prefers-color-scheme: dark) { + :root { + --grid-background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABYWlDQ1BrQ0dDb2xvclNwYWNlRGlzcGxheVAzAAAokWNgYFJJLCjIYWFgYMjNKykKcndSiIiMUmB/yMAOhLwMYgwKicnFBY4BAT5AJQwwGhV8u8bACKIv64LMOiU1tUm1XsDXYqbw1YuvRJsw1aMArpTU4mQg/QeIU5MLikoYGBhTgGzl8pICELsDyBYpAjoKyJ4DYqdD2BtA7CQI+whYTUiQM5B9A8hWSM5IBJrB+API1klCEk9HYkPtBQFul8zigpzESoUAYwKuJQOUpFaUgGjn/ILKosz0jBIFR2AopSp45iXr6SgYGRiaMzCAwhyi+nMgOCwZxc4gxJrvMzDY7v////9uhJjXfgaGjUCdXDsRYhoWDAyC3AwMJ3YWJBYlgoWYgZgpLY2B4dNyBgbeSAYG4QtAPdHFacZGYHlGHicGBtZ7//9/VmNgYJ/MwPB3wv//vxf9//93MVDzHQaGA3kAFSFl7jXH0fsAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAACNJREFUSA3t0IEAAAAMBKFHm7/UTaQQWnXDgAEDBgwYMGDgAXaJAz4RVVHYAAAAAElFTkSuQmCC"); + --background-color-default-brightness: 2%; + --background-color-light-brightness: 6%; + --border-brightness: 5%; + --background-color-default: hsl(225 50% var(--background-color-default-brightness) / 1); + --background-color-light: hsl(225 50% var(--background-color-light-brightness) / 1); + --border-color: hsl(225 25% var(--border-brightness) / 1); + } + #logo { + height: 28px; + margin-right: 4px; + } + #authenticated, + #branding { + opacity: 0.5; + transition: 125ms opacity ease-in-out; + } + #authenticated:hover, + #branding:hover { + opacity: 1; + } + #branding > span { + padding: 8px; + } + body, + html { + margin: 0; + padding: 0; + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; + } + * { + font-family: "Proxima Nova", "Ubiquity Nova", sans-serif; + color: #fff; + font-weight: 500; + } + @font-face { + font-family: "Ubiquity Nova"; + font-style: normal; + font-weight: 400; + src: url(fonts/ubiquity-nova-standard.eot); + src: + url(fonts/ubiquity-nova-standard.eot#iefix) format("embedded-opentype"), + url(fonts/ubiquity-nova-standard.woff) format("woff"), + url(fonts/ubiquity-nova-standard.ttf) format("truetype"); + } + #issues-container { + max-width: 640px; + scrollbar-width: none; + -ms-overflow-style: none; + padding: 56px 0; + transition: 0.25s all cubic-bezier(0, 1, 1, 1); + } + body.preview-active #issues-container { + transform: translateX(-50%); + } + &::-webkit-scrollbar { + display: none; + } + #issues-container .issue-element-inner { + transition: 125ms opacity ease-out 62.5ms; + } + #issues-container:hover .issue-element-inner { + opacity: 0.5; + } + #issues-container.keyboard-selection:hover .issue-element-inner { + opacity: 1; + } + #issues-container * { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + #issues-container > div:first-child { + border-top: 1px solid #80808020; + } + #issues-container > div:last-child { + border-top: 1px solid #80808020; + } + #issues-container > div { + padding: 0 16px; + overflow: hidden; + background-size: 56px; + background-position: 0; + background-repeat: no-repeat; + opacity: 0.125; + margin: 3px auto; + border: 1px solid #80808020; + border-radius: 4px; + cursor: pointer; + margin: 4px; + filter: blur(4px); + display: block; + /* border-color: var(--border-color); */ + /* box-shadow: inset 0 0 24px #00bfff10; */ + } + #issues-container > div.selected, + #issues-container > div.selected .info, + #issues-container > div.selected h3 { + opacity: 1; + } + #issues-container.keyboard-selection > div.active { + opacity: 0.33; + } + #issues-container.keyboard-selection > div.active.selected { + opacity: 1; + } + #issues-container > div.active { + transition: 125ms all ease-in-out; + opacity: 1; + filter: blur(0); + } + #issues-container > div:hover { + opacity: 1; + transition: background-color 0s; + } + #issues-container > div:hover .info { + opacity: 1; + } + #issues-container > div:hover .issue-element-inner { + transition: 125ms opacity ease-in-out; + opacity: 1; + } + #issues-container > div:active { + background-color: #80808040; + } + #issues-container h3 { + line-height: 1; + font-size: 16px; + text-overflow: ellipsis; + margin: 0; + } + #issues-container p { + text-align: right; + } + p { + margin: 0; + line-height: 1; + color: #bfbfbf; + font-size: 12px; + letter-spacing: 0.5px; + text-rendering: geometricPrecision; + top: 0; + right: 0; + } + li { + margin: 0; + line-height: 1; + color: #bfbfbf; + font-size: 12px; + letter-spacing: 0.5px; + text-rendering: geometricPrecision; + top: 0; + right: 0; + } + .issue-element-inner { + position: relative; + text-align: left; + display: flex; + padding: 12px 0; + align-items: center; + } + p.organization-name { + opacity: 0.5; + display: inline-block; + } + p.repository-name { + display: inline-block; + } + label:first-child { + margin-left: 0; + } + label { + padding: 4px 6px; + border-radius: 4px; + margin: 0 4px 0; + font-size: 12px; + text-align: center; + white-space: nowrap; + background-color: #80808020; + width: 64px; + letter-spacing: 0.5px; + flex-grow: 4; + justify-content: center; + display: flex; + align-items: center; + height: 16px; + text-overflow: ellipsis; + overflow: hidden; + display: inline-block; + } + input[type="radio"] { + background-color: unset; + cursor: default; + appearance: unset; + display: none; + box-sizing: border-box; + margin: 0; + padding: 0; + border: none; + } + #filters label { + cursor: pointer; + min-width: fit-content; + } + #filters label:hover { + background-color: #80808040; + } + #filters label:active { + background-color: #80808080; + } + input[type="radio"]:checked + label { + background-color: #80808080 !important; + } + .labels { + display: flex; + margin-left: auto; + } + .partner { + bottom: 0; + margin: 0; + } + body { + display: flex; + align-items: center; + align-content: center; + justify-content: center; + } + .info { + opacity: 0.66; + transition: 125ms opacity ease-in-out; + align-items: center; + } + #issues-container > .issue-element-wrapper { + opacity: 0.5; + transition: 0.33s all cubic-bezier(0, 1, 1, 1); + filter: blur(8px); + } + button { + appearance: unset; + text-rendering: unset; + color: unset; + letter-spacing: unset; + word-spacing: unset; + line-height: unset; + text-transform: unset; + text-indent: unset; + text-shadow: unset; + display: unset; + text-align: unset; + align-items: unset; + cursor: pointer; + box-sizing: unset; + background-color: unset; + margin: unset; + padding-block: unset; + padding-inline: unset; + border-width: unset; + border-style: unset; + border-color: unset; + border-image: unset; + border: 1px solid #80808020; + padding: 8px 16px; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 1px; + background-color: #80808010; + } + button:hover { + background-color: #80808020; + } + button:active { + background-color: #80808040; + } + .toolbar { + position: fixed; + width: calc(100vw - 8px); + height: 48px; + top: 0; + backdrop-filter: blur(8px); + padding: 4px; + display: flex; + border-bottom: 1px solid #80808020; + justify-content: center; + -webkit-backdrop-filter: blur(8px); + justify-content: space-between; + justify-content: space-evenly; + } + .toolbar > * { + align-items: center; + display: inline-flex; + text-align: left; + margin: 0 16px; + } + #authenticated > * { + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0 4px; + max-height: 48px; + } + #authenticated > img { + border-radius: 50%; + height: 40px; + padding: 4px 0; + } + .toolbar[data-authenticated="true"] > #github-login-button { + display: none; + } + .toolbar { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + opacity: 0; + transition: 0.5s opacity ease-in-out; + } + .toolbar.ready { + opacity: 1; + } + #branding { + text-transform: uppercase; + letter-spacing: 1.5px; + font-weight: 400; + text-rendering: geometricPrecision; + } + .full { + display: unset !important; + } + @media screen and (max-width: 804px) { + .mid { + display: none !important; + } + #branding > span { + padding: 0; + } + } + @media screen and (max-width: 1139px) { + .full { + display: none !important; + } + .issue-element-inner { + display: block; + } + #issues-container .labels { + margin-top: 4px; + } + #branding { + margin: auto; + margin-left: 12px; + } + .toolbar { + overflow-x: auto; + margin: 0; + padding-left: 0; + display: flex; + align-items: center; + justify-content: center; + } + .toolbar > * { + margin-left: 12px; + } + .toolbar button { + /* padding: 12px 16px; */ + height: 24px !important; + padding: 4px 16px; + line-height: 1; + } + } + @media screen and (max-width: 1280px) { + body.preview-active { + overflow: hidden; + } + .preview.active { + transform: translateX(0) !important; + } + body.preview-active #issues-container { + transform: translateX(0) !important; + } + body.preview-active > #issues-container > div, + body.preview-active > background { + filter: blur(8px); + pointer-events: none; + } + } + .toolbar > button { + text-align: center; + } + #filters button { + margin-top: 8px; + margin-right: 8px; + } + #issues-container img { + height: 24px; + border-radius: 24px; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + opacity: 0; + transition: 0.25s opacity ease-in-out; + } + #issues-container img[src] { + opacity: 1; + } + + background, + background #grid { + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + } + background #grid { + /* -webkit-mask-image: radial-gradient(#00000020 0, #00000080 100%); */ + /* mask-image: radial-gradient(#00000020 0, #00000080 100%); */ + pointer-events: none; + } + .preview blockquote { + margin-left: 16px; + opacity: 0.66; + } + .preview sup { + display: block; + } + .preview hr { + border-color: #ffffff40; + } + .preview p { + line-height: 1.25; + word-break: break-word; + margin-bottom: 12px; + letter-spacing: 0; + font-size: 16px; + } + .preview img { + max-width: 100%; + } + .preview pre { + position: relative; + display: inline-block; + width: 100%; + overflow: scroll; + height: 48px; + } + .preview pre code { + position: absolute; + font-family: monospace; + } + .preview ol, + .preview ul { + padding-left: 12px; + margin-left: 4px; + } + .preview h1 { + margin: 8px; + font-size: 24px; + margin-block-start: 0; + margin-block-end: 0; + margin-inline: 0; + margin-inline-end: 0; + } + .preview a { + word-break: break-all; + } + .preview a[href*="//"] .open-new-link svg + { + fill: #ffffff80; + vertical-align: middle; + height: 20px; + } + .preview-body-inner { + line-height: 1.25; + } + .preview-body-inner > :last-child { + margin-bottom: 0; + } + .preview { + text-align: left; + position: fixed; + transition: 0.25s all cubic-bezier(0, 1, 1, 1); + max-width: 640px; + border: 1px solid #80808020; + border-radius: 4px; + opacity: 0; + top: 60px; + /* flex-direction: row; */ + /* display: flex; */ + /* transform: translateX(0); */ + /* flex-wrap: wrap; */ + pointer-events: none; + /* overflow: hidden; */ + width: calc(100vw - 14px); + } + .preview * { + pointer-events: none; + } + .preview.active { + pointer-events: all; + } + .preview.active * { + pointer-events: all; + } + .close-preview svg { + fill: #ffffff80; + vertical-align: middle; + height: 36px; + } + .preview.active { + opacity: 1; + pointer-events: all; + transform: translateX(50%); + /* box-shadow: 0 0px 16px #ffffff10; */ + } + .preview-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 16px; + } + .preview-header > a { + word-wrap: initial; + text-decoration: none; + opacity: 0.75; + display: flex; + justify-content: right; + align-items: center; + width: calc(100% - 72px); + } + .preview-header > a:hover { + opacity: 1; + } + .preview-header > a > h1 { + word-wrap: initial; + word-break: normal; + display: inline; + vertical-align: middle; + margin-right: 12px; + text-align: right; + font-size: 20px; + text-align: right; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .preview-body { + overflow: scroll; + margin: 0; + border-top: 1px solid #202020; + padding: 16px; + max-height: calc(100vh - (80px)); + max-height: calc(100vh - (242px)); + /* border-color: var(--border-color); */ + } + .preview-content { + width: 100%; + } + .preview button.close-preview { + right: 0; + top: 0; + margin-right: 8px; + padding: 8px 8px; + border: none; + background: 0 0; + padding: 0; + } + .preview button.close-preview:hover svg { + fill: #fff; + } + .preview li { + margin-bottom: 8px; + letter-spacing: 0; + font-size: 16px; + line-height: 1.25; + } + #filters input[type="text"] { + margin: 0 4px; + padding: 4px; + border: none; + border-radius: 4px; + background-color: #80808020; + width: 100%; + height: 16px; + text-align: center; + min-width: 80px; + } + #filters input[type="text"]::placeholder { + font-size: 12px; + letter-spacing: 0.5px; + } + #filters input[type="text"].hidden { + display: none; + } + button#github-login-button { + margin-left: 0; + } + + p.organization-name::after { + content: "/"; + } + #loader { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: #00000080; + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + } + + input[type="radio"][data-ordering="reverse"]:not(#leaderboard) + label::after { + margin-left: 6px; + content: "▲"; + } + + input[type="radio"][data-ordering="normal"]:not(#leaderboard) + label::after { + margin-left: 6px; + content: "▼"; + } + + #bottom-bar { + top: unset; + bottom: 0; + opacity: 1; + border-top: 1px solid #80808020; + border-bottom: unset; + } + #bottom-bar a { + text-decoration: none; + } + #bottom-bar a button { + height: 30px; + } + .toolbar button { + white-space: nowrap; + } + + .info .entry-title { + font-size: 16px; + line-height: 1.25; + margin: 0; + margin-bottom: 4px; + display: grid; + grid-template-columns: 1fr 1fr; + } + + .info .entry-body { + width: 100%; + font-size: 12px; + line-height: 1.25; + margin: 0; + margin-bottom: 4px; + display: grid; + grid-template-columns: 1fr 1fr; + } + + background #grid { + pointer-events: none; + } + + background #grid canvas { + width: 100%; + height: 100%; + opacity: 0; + animation: background-grid-fade-in 3s ease-in-out forwards; + } + + background .gradient { + width: 200vw; + height: 200vh; + position: absolute; + opacity: 0; + } + .grid-loaded background .gradient { + background-image: radial-gradient(#00bfff00 0%, #00bfffff 15%, #00bfff00 34%, #00bfffff 58%, #00bfff00 75%, #00bfffff 100%); + animation: background-gradients-fade-in 3s ease-in-out forwards; + } + background > :nth-child(1) { + transform: translateX(-100vw); + } + background > :nth-child(2) { + transform: translateY(-50vh); + } + + @keyframes background-gradients-fade-in { + to { + opacity: 0.125; + } + } + @keyframes background-grid-fade-in { + to { + opacity: 0.5; + } + } +} diff --git a/yarn.lock b/yarn.lock index 0c43b39..d4394af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1580,6 +1580,101 @@ dependencies: which "^4.0.0" +"@octokit/auth-token@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-4.0.0.tgz#40d203ea827b9f17f42a29c6afb93b7745ef80c7" + integrity sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA== + +"@octokit/core@^5.0.2": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-5.2.0.tgz#ddbeaefc6b44a39834e1bb2e58a49a117672a7ea" + integrity sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg== + dependencies: + "@octokit/auth-token" "^4.0.0" + "@octokit/graphql" "^7.1.0" + "@octokit/request" "^8.3.1" + "@octokit/request-error" "^5.1.0" + "@octokit/types" "^13.0.0" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^9.0.1": + version "9.0.5" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-9.0.5.tgz#e6c0ee684e307614c02fc6ac12274c50da465c44" + integrity sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw== + dependencies: + "@octokit/types" "^13.1.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-7.1.0.tgz#9bc1c5de92f026648131f04101cab949eeffe4e0" + integrity sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ== + dependencies: + "@octokit/request" "^8.3.0" + "@octokit/types" "^13.0.0" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^22.2.0": + version "22.2.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" + integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg== + +"@octokit/plugin-paginate-rest@11.3.1": + version "11.3.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.1.tgz#fe92d04b49f134165d6fbb716e765c2f313ad364" + integrity sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g== + dependencies: + "@octokit/types" "^13.5.0" + +"@octokit/plugin-request-log@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz#98a3ca96e0b107380664708111864cb96551f958" + integrity sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA== + +"@octokit/plugin-rest-endpoint-methods@13.2.2": + version "13.2.2" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.2.tgz#af8e5dd2cddfea576f92ffaf9cb84659f302a638" + integrity sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA== + dependencies: + "@octokit/types" "^13.5.0" + +"@octokit/request-error@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.1.0.tgz#ee4138538d08c81a60be3f320cd71063064a3b30" + integrity sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q== + dependencies: + "@octokit/types" "^13.1.0" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^8.3.0", "@octokit/request@^8.3.1": + version "8.4.0" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-8.4.0.tgz#7f4b7b1daa3d1f48c0977ad8fffa2c18adef8974" + integrity sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw== + dependencies: + "@octokit/endpoint" "^9.0.1" + "@octokit/request-error" "^5.1.0" + "@octokit/types" "^13.1.0" + universal-user-agent "^6.0.0" + +"@octokit/rest@^20.0.2": + version "20.1.1" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-20.1.1.tgz#ec775864f53fb42037a954b9a40d4f5275b3dc95" + integrity sha512-MB4AYDsM5jhIHro/dq4ix1iWTLGToIGk6cWF5L6vanFaMble5jTX/UBQyiv05HsWnwUtY8JrfHy2LWfKwihqMw== + dependencies: + "@octokit/core" "^5.0.2" + "@octokit/plugin-paginate-rest" "11.3.1" + "@octokit/plugin-request-log" "^4.0.0" + "@octokit/plugin-rest-endpoint-methods" "13.2.2" + +"@octokit/types@^13.0.0", "@octokit/types@^13.1.0", "@octokit/types@^13.5.0": + version "13.5.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.5.0.tgz#4796e56b7b267ebc7c921dcec262b3d5bfb18883" + integrity sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ== + dependencies: + "@octokit/openapi-types" "^22.2.0" + "@open-draft/deferred-promise@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz#4a822d10f6f0e316be4d67b4d4f8c9a124b073bd" @@ -1743,6 +1838,63 @@ ignore "^5.1.8" p-map "^4.0.0" +"@supabase/auth-js@2.64.2": + version "2.64.2" + resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.64.2.tgz#fe6828ed2c9844bf2e71b27f88ddfb635f24d1c1" + integrity sha512-s+lkHEdGiczDrzXJ1YWt2y3bxRi+qIUnXcgkpLSrId7yjBeaXBFygNjTaoZLG02KNcYwbuZ9qkEIqmj2hF7svw== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/functions-js@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.3.1.tgz#bddc12a97872f3978a733b66bddac53370721589" + integrity sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/node-fetch@2.6.15", "@supabase/node-fetch@^2.6.14": + version "2.6.15" + resolved "https://registry.yarnpkg.com/@supabase/node-fetch/-/node-fetch-2.6.15.tgz#731271430e276983191930816303c44159e7226c" + integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ== + dependencies: + whatwg-url "^5.0.0" + +"@supabase/postgrest-js@1.15.2": + version "1.15.2" + resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz#c0a725706e3d534570d014d7b713cea12553ab98" + integrity sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/realtime-js@2.9.5": + version "2.9.5" + resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.9.5.tgz#22b7de952a7f37868ffc25d32d19f03f27bfcb40" + integrity sha512-TEHlGwNGGmKPdeMtca1lFTYCedrhTAv3nZVoSjrKQ+wkMmaERuCe57zkC5KSWFzLYkb5FVHW8Hrr+PX1DDwplQ== + dependencies: + "@supabase/node-fetch" "^2.6.14" + "@types/phoenix" "^1.5.4" + "@types/ws" "^8.5.10" + ws "^8.14.2" + +"@supabase/storage-js@2.5.5": + version "2.5.5" + resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.5.5.tgz#2958e2a2cec8440e605bb53bd36649288c4dfa01" + integrity sha512-OpLoDRjFwClwc2cjTJZG8XviTiQH4Ik8sCiMK5v7et0MDu2QlXjCAW3ljxJB5+z/KazdMOTnySi+hysxWUPu3w== + dependencies: + "@supabase/node-fetch" "^2.6.14" + +"@supabase/supabase-js@^2.39.0": + version "2.43.4" + resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.43.4.tgz#62c956b16bb01d5cb59e3ad73cf7628e3e9835c0" + integrity sha512-/pLPaxiIsn5Vaz3s32HC6O/VNwfeddnzS0bZRpOW0AKcPuXroD8pT9G8mpiBlZfpKsMmq6k7tlhW7Sr1PAQ1lw== + dependencies: + "@supabase/auth-js" "2.64.2" + "@supabase/functions-js" "2.3.1" + "@supabase/node-fetch" "2.6.15" + "@supabase/postgrest-js" "1.15.2" + "@supabase/realtime-js" "2.9.5" + "@supabase/storage-js" "2.5.5" + "@types/babel__core@^7.1.14": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -1861,6 +2013,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== +"@types/phoenix@^1.5.4": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.4.tgz#cceac93a827555473ad38057d1df7d06eef1ed71" + integrity sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA== + "@types/picomatch@2.3.3": version "2.3.3" resolved "https://registry.yarnpkg.com/@types/picomatch/-/picomatch-2.3.3.tgz#be60498568c19e989e43fb39aa84be1ed3655e92" @@ -1906,6 +2063,13 @@ resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g== +"@types/ws@^8.5.10": + version "8.5.10" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" + integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -2335,6 +2499,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +before-after-hook@^2.2.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" + integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== + blob-util@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" @@ -3092,6 +3261,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +deprecation@^2.0.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + detect-newline@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" @@ -3130,10 +3304,10 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" -dotenv@^16.4.4: - version "16.4.4" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.4.tgz#a26e7bb95ebd36272ebb56edb80b826aecf224c1" - integrity sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg== +dotenv@^16.3.1, dotenv@^16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== eastasianwidth@^0.2.0: version "0.2.0" @@ -3293,6 +3467,13 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" +esbuild-plugin-env@^1.0.8: + version "1.1.1" + resolved "https://registry.yarnpkg.com/esbuild-plugin-env/-/esbuild-plugin-env-1.1.1.tgz#6501270b83824aa1b8d21f31657e1bbeb4f78c70" + integrity sha512-xM8peq9LLzvrQ/1czyj1HLhbJgtD0UzlKuNuTvPySdFGc77ysYsqo3oxDBOyQVSaa4YQU4wjrPpbeeDX1z0eHA== + dependencies: + dotenv "^16.4.5" + esbuild@^0.20.1: version "0.20.1" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.1.tgz#1e4cbb380ad1959db7609cb9573ee77257724a3e" @@ -5335,6 +5516,11 @@ map-obj@^4.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== +marked@^11.0.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-11.2.0.tgz#fc908aeca962b721b0392ee4205e6f90ebffb074" + integrity sha512-HR0m3bvu0jAPYiIvLUUQtdg1g6D247//lvcekpHO1WMvbwDlwSkZAX9Lw4F4YHE1T0HaaNve0tuAWuV1UJ6vtw== + md5@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" @@ -6816,6 +7002,11 @@ tough-cookie@^4.1.3: universalify "^0.2.0" url-parse "^1.5.3" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + trim-newlines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144" @@ -7006,6 +7197,11 @@ unique-string@^3.0.0: dependencies: crypto-random-string "^4.0.0" +universal-user-agent@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.1.tgz#15f20f55da3c930c57bddbf1734c6654d5fd35aa" + integrity sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -7130,6 +7326,19 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -7232,6 +7441,11 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@^8.14.2: + version "8.17.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" + integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== + xdg-basedir@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz#1efba19425e73be1bc6f2a6ceb52a3d2c884c0c9"