Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

leaderboard init #1

Open
wants to merge 7 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}"]
Expand Down
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
MY_SECRET="MY_SECRET"
SUPABASE_DB_PASSWORD=
SUPABASE_PROJECT_ID=
SUPABASE_ACCESS_TOKEN=
36 changes: 7 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -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?
83 changes: 64 additions & 19 deletions build/esbuild-build.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>): Record<string, string> {
const defines: Record<string, string> = {};
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);
}
49 changes: 49 additions & 0 deletions build/plugins/invert-colors.ts
Original file line number Diff line number Diff line change
@@ -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" };
});
},
};
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions src/home/authentication.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
78 changes: 78 additions & 0 deletions src/home/getters/get-github-access-token.ts
Original file line number Diff line number Diff line change
@@ -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<string | null> {
// 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;
};
}
42 changes: 42 additions & 0 deletions src/home/getters/get-github-user.ts
Original file line number Diff line number Diff line change
@@ -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<GitHubUser | null> {
const activeSessionToken = await getSessionToken();
if (activeSessionToken) {
return getNewGitHubUser(activeSessionToken);
} else {
return null;
}
}

async function getSessionToken(): Promise<string | null> {
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<string | null> {
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<GitHubUser> {
const octokit = new Octokit({ auth: providerToken });
const response = (await octokit.request("GET /user")) as GitHubUserResponse;
return response.data;
}
Loading
Loading