Skip to content

Commit e874c10

Browse files
Add changeset (#76)
2 parents 26e39bf + 3665523 commit e874c10

File tree

8 files changed

+165
-17
lines changed

8 files changed

+165
-17
lines changed

.changeset/gold-ants-hide.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eth-tech-tree": minor
3+
---
4+
5+
Added leaderboard view

src/actions/submit-challenge.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,14 @@ export async function submitChallenge(name: string, contractAddress?: string) {
2121
// Send the contract address to the server
2222
const response = await submitChallengeToServer(userAddress as string, "sepolia", name, contractAddress as string);
2323
if (response.result) {
24-
const { passed, failingTests } = response.result;
24+
const { passed, failingTests, error } = response.result;
2525
if (passed) {
2626
console.log("Challenge passed tests! Congratulations!");
27-
// TODO: Update user state and reflect in tree
2827
} else {
28+
if (error) {
29+
console.log("The testing server encountered an error when running this test:");
30+
console.log(chalk.red(error));
31+
}
2932
console.log("Failing tests:", Object.keys(failingTests).length);
3033
for (const testName in failingTests) {
3134
console.log(chalk.blue(testName), chalk.red(failingTests[testName].reason));

src/index.ts

+21-10
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { getUser } from "./modules/api";
77
import { setupChallenge, submitChallenge } from "./actions";
88
import select from './utils/global-context-select-list';
99
import { ProgressView } from "./utils/progress-view";
10-
import { calculatePoints } from "./utils/helpers";
10+
import { calculatePoints, stripAnsi } from "./utils/helpers";
11+
import { LeaderboardView } from "./utils/leaderboard-view";
12+
import { fetchLeaderboard } from "./modules/api";
1113

1214
type GlobalChoice = {
1315
value: string;
@@ -30,6 +32,7 @@ export class TechTree {
3032
{ value: 'quit', key: 'q' },
3133
{ value: 'help', key: 'h' },
3234
{ value: 'progress', key: 'p' },
35+
{ value: 'leaderboard', key: 'l' },
3336
{ value: 'back', key: 'escape' },
3437
{ value: 'back', key: 'backspace' },
3538
];
@@ -73,7 +76,7 @@ export class TechTree {
7376
};
7477

7578
try {
76-
this.nodeLabel = node.label;
79+
this.nodeLabel = node.shortname || node.label;
7780
this.clearView();
7881
this.printMenu();
7982
const { answer } = await select(directionsPrompt);
@@ -87,8 +90,11 @@ export class TechTree {
8790
await selectedAction();
8891
}
8992
} catch (error) {
90-
// Do nothing
91-
// console.log(error);
93+
if (error instanceof Error && error.name === 'ExitPromptError') {
94+
// Because canceling the promise can cause the inquirer prompt to throw we need to silence this error
95+
} else {
96+
throw error;
97+
}
9298
}
9399
}
94100

@@ -101,6 +107,8 @@ export class TechTree {
101107
return () => this.printHelp();
102108
} else if (selectedActionLabel === 'progress') {
103109
return () => this.printProgress();
110+
} else if (selectedActionLabel === 'leaderboard') {
111+
return () => this.printLeaderboard();
104112
}
105113
throw new Error(`Invalid global choice: ${selectedActionLabel}`);
106114
}
@@ -375,8 +383,8 @@ Open up the challenge in your favorite code editor and follow the instructions i
375383

376384
const width = process.stdout.columns;
377385
const userInfo = `${chalk.green(user)} ${chalk.yellow(`(${points} points)`)}`;
378-
const topMenuText = chalk.bold(`${borderLeft}${currentViewName}${new Array(width - (this.stripAnsi(currentViewName).length + this.stripAnsi(userInfo).length + 4)).fill(border).join('')}${userInfo}${borderRight}`);
379-
const bottomMenuText = chalk.bold(`${borderLeft}${chalk.bgBlue(`<q>`)} to quit | ${chalk.bgBlue(`<Esc>`)} to go back | ${chalk.bgBlue(`<p>`)} view progress${new Array(width - 54).fill(border).join('')}${borderRight}`);
386+
const topMenuText = chalk.bold(`${borderLeft}${currentViewName}${new Array(width - (stripAnsi(currentViewName).length + stripAnsi(userInfo).length + 4)).fill(border).join('')}${userInfo}${borderRight}`);
387+
const bottomMenuText = chalk.bold(`${borderLeft}${chalk.bgBlue(`<q>`)} to quit | ${chalk.bgBlue(`<Esc>`)} to go back | ${chalk.bgBlue(`<p>`)} view progress | ${chalk.bgBlue(`<l>`)} leaderboard${new Array(width - 72).fill(border).join('')}${borderRight}`);
380388

381389
// Save cursor position
382390
process.stdout.write('\x1B7');
@@ -401,10 +409,6 @@ Open up the challenge in your favorite code editor and follow the instructions i
401409
process.stdout.write('\x1B[?25h');
402410
}
403411

404-
stripAnsi(text: string): string {
405-
return text.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
406-
}
407-
408412
getMaxViewHeight(): number {
409413
const maxRows = 20;
410414
if (process.stdout.rows < maxRows) {
@@ -428,4 +432,11 @@ Open up the challenge in your favorite code editor and follow the instructions i
428432
const progressTree = progressView.buildProgressTree();
429433
await this.navigate(progressTree);
430434
}
435+
436+
async printLeaderboard(): Promise<void> {
437+
const leaderboardData = await fetchLeaderboard();
438+
const leaderboardView = new LeaderboardView(leaderboardData, this.userState.address);
439+
const leaderboardTree = leaderboardView.buildLeaderboardTree();
440+
await this.navigate(leaderboardTree);
441+
}
431442
}

src/modules/api.ts

+14
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,18 @@ export const submitChallengeToServer = async (userAddress: string, network: stri
7171
console.error('Error:', error);
7272
return {};
7373
}
74+
};
75+
76+
/**
77+
* Fetch Leaderboard
78+
*/
79+
export const fetchLeaderboard = async () => {
80+
try {
81+
const response = await fetch(`${API_URL}/leaderboard`);
82+
const data = await response.json();
83+
return data.leaderboard;
84+
} catch (error) {
85+
console.error('Error:', error);
86+
return [];
87+
}
7488
};

src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export type Actions = {
4949

5050
export type TreeNode = {
5151
label: string;
52+
shortname?: string;
5253
name: string;
5354
children: TreeNode[];
5455
type: "header" | "challenge" | "quiz" | "capstone-project";

src/utils/helpers.ts

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ export const getDevice = (): string => {
4141
return `${hostname}(${platform}:${arch})`;
4242
}
4343

44+
export const stripAnsi = (text: string): string => {
45+
return text.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
46+
}
47+
4448
export const calculatePoints = (completedChallenges: Array<{ challenge: IChallenge | undefined, completion: any }>): number => {
4549
const pointsPerLevel = [100, 150, 225, 300, 400, 500];
4650
return completedChallenges

src/utils/leaderboard-view.ts

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { TreeNode } from "../types";
2+
import chalk from "chalk";
3+
import { stripAnsi } from "./helpers";
4+
5+
interface LeaderboardEntry {
6+
address: string;
7+
ens: string | null;
8+
challengesCompleted: number;
9+
points: number;
10+
totalGasUsed: number;
11+
rank: number;
12+
}
13+
14+
export class LeaderboardView {
15+
constructor(private leaderboard: LeaderboardEntry[], private userAddress: string) {
16+
this.userAddress = userAddress;
17+
}
18+
19+
buildLeaderboardTree(): TreeNode {
20+
// Create individual entry nodes for each leaderboard position
21+
const entryNodes: TreeNode[] = this.leaderboard.map(entry => ({
22+
type: "header",
23+
label: this.getEntryLabel(entry),
24+
shortname: entry.ens || entry.address,
25+
name: `rank-${entry.rank}`,
26+
children: [],
27+
message: this.buildEntryMessage(entry)
28+
}));
29+
30+
// Create main leaderboard node
31+
return {
32+
type: "header",
33+
label: "Leaderboard",
34+
name: "leaderboard",
35+
children: entryNodes,
36+
message: this.buildLeaderboardMessage()
37+
};
38+
}
39+
40+
private getEntryLabel(entry: LeaderboardEntry): string {
41+
const identifier = entry.ens || entry.address;
42+
return chalk.white(`${this.getRankFormatting(entry.rank)} | ${this.formatSpacing(chalk.yellow(entry.points.toLocaleString()), 8)} | ${chalk.green(identifier)}`);
43+
}
44+
45+
private getRankFormatting(rank: number): string {
46+
let rankString = rank.toString();
47+
if (rank === 1) rankString = "🥇 " + rankString;
48+
if (rank === 2) rankString = "🥈 " + rankString;
49+
if (rank === 3) rankString = "🥉 " + rankString;
50+
return chalk.bold(this.formatSpacing(rankString, 5, false));
51+
}
52+
53+
private buildEntryMessage(entry: LeaderboardEntry): string {
54+
return `${chalk.bold(`Rank ${entry.rank}`)}
55+
Points: ${chalk.yellow(entry.points.toLocaleString())}
56+
Challenges Completed: ${chalk.blue(entry.challengesCompleted.toString())}
57+
Total Gas Used: ${chalk.green(entry.totalGasUsed.toLocaleString())}
58+
`;
59+
}
60+
61+
private formatSpacing(text: string, width: number, center: boolean = true): string {
62+
const strippedText = stripAnsi(text);
63+
const textLength = strippedText.length;
64+
const totalPadding = width - textLength;
65+
66+
if (totalPadding <= 0) return text;
67+
68+
if (!center) {
69+
return " ".repeat(totalPadding) + text;
70+
}
71+
72+
const leftPadding = Math.ceil(totalPadding / 2);
73+
const rightPadding = Math.floor(totalPadding / 2);
74+
75+
return " ".repeat(leftPadding) + text + " ".repeat(rightPadding);
76+
}
77+
78+
private getUserInfo(): LeaderboardEntry | undefined {
79+
return this.leaderboard.find(entry => entry.address === this.userAddress);
80+
}
81+
82+
private buildLeaderboardMessage(): string {
83+
const userInfo = this.getUserInfo();
84+
let statement = "Complete challenges to score points and climb the leaderboard!";
85+
if (userInfo) {
86+
switch (userInfo.rank) {
87+
case 1:
88+
statement = "You are the TOP DOG!";
89+
break;
90+
case 2:
91+
statement = "You are second place!\nKeep challenging yourself and see if you can catch up to the leader!\nTry to improve the gas efficiency of your past solutions.";
92+
break;
93+
case 3:
94+
statement = "You are third place!\nKeep challenging yourself and see if you can catch up to the leader!\nTry to improve the gas efficiency of your past solutions.";
95+
break;
96+
case 4:
97+
case 5:
98+
case 6:
99+
case 7:
100+
case 8:
101+
case 9:
102+
case 10:
103+
statement = `You are in the top ten!\nKeep challenging yourself and see if you can climb the leaderboard!\nTry to improve the gas efficiency of your past solutions.`;
104+
break;
105+
default:
106+
statement = `You are rank ${userInfo.rank}.\nKeep challenging yourself and see if you can climb the leaderboard!`;
107+
break;
108+
}
109+
}
110+
111+
return chalk.bold(`${statement}\n\nTop Players\n Rank | Points | Player`);
112+
}
113+
}

src/utils/progress-view.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,15 @@ export class ProgressView {
5353
private buildStatsMessage(points: number, completionRate: string): string {
5454
const totalChallenges = this.challenges.filter(c => c.enabled).length;
5555
const completedChallenges = this.userState.challenges.filter(c => c.status === "success").length;
56-
return `${chalk.bold("Your Progress")}
57-
58-
Address: ${chalk.green(this.userState.ens || this.userState.address)}
56+
return `Address: ${chalk.green(this.userState.ens || this.userState.address)}
5957
${chalk.yellow(`Points Earned: ${points.toLocaleString()}`)}
6058
6159
Challenges Completed: ${chalk.blue(`${completedChallenges}/${totalChallenges} (${completionRate}%)`)}
6260
${completedChallenges ? "Details:" : ""}`;
6361
}
6462

6563
private buildChallengeMessage(challenge: IChallenge, completion: any): string {
66-
let message = `${chalk.bold(challenge.label)}\n\n`;
67-
message += `Description: ${challenge.description}\n\n`;
64+
let message = `Description: ${challenge.description}\n\n`;
6865
message += `Completion Date: ${chalk.blue(new Date(completion.timestamp).toLocaleString())}\n`;
6966

7067
if (completion.contractAddress) {

0 commit comments

Comments
 (0)