Skip to content

Commit

Permalink
Add changeset (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
escottalexander authored Oct 3, 2024
2 parents 6735dd8 + d5ccc4b commit 7222b39
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/shy-bats-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eth-tech-tree": patch
---

ui improvements
4 changes: 2 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { promptForMissingUserState } from "./tasks/prompt-for-missing-user-state";
import { renderIntroMessage } from "./tasks/render-intro-message";
import type { Args, UserState } from "./types";
import type { Args, IUser } from "./types";
import { startVisualization } from "./utils/tree";
import { loadUserState, saveChallenges } from "./utils/stateManager";
import { fetchChallenges } from "./modules/api";
Expand All @@ -23,7 +23,7 @@ export async function cli(args: Args) {
}
}

async function init(userState: UserState) {
async function init(userState: IUser) {
// Use local user state or prompt to retrieve user from server
await promptForMissingUserState(userState);

Expand Down
4 changes: 2 additions & 2 deletions src/tasks/parse-command-arguments-and-options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import arg from "arg";
import { UserState } from "../types";
import { IUser } from "../types";
import fs from "fs";
import inquirer from "inquirer";
import { isValidAddress } from "../utils/helpers";
Expand Down Expand Up @@ -75,7 +75,7 @@ export async function parseCommandArgumentsAndOptions(
return argumentObject as CommandOptions;
}

export async function promptForMissingCommandArgs(commands: CommandOptions, userState: UserState): Promise<CommandOptions> {
export async function promptForMissingCommandArgs(commands: CommandOptions, userState: IUser): Promise<CommandOptions> {
const cliAnswers = Object.fromEntries(
Object.entries(commands).filter(([key, value]) => value !== null)
);
Expand Down
14 changes: 6 additions & 8 deletions src/tasks/prompt-for-missing-user-state.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { getUser, upsertUser } from "../modules/api";
import {
UserState
} from "../types";
import { IUser } from "../types";
import inquirer from "inquirer";
import { saveUserState } from "../utils/stateManager";
import { isValidAddressOrENS, getDevice, checkValidPathOrCreate, isValidAddress } from "../utils/helpers";

// default values for unspecified args
const defaultOptions: Partial<UserState> = {
const defaultOptions: Partial<IUser> = {
installLocation: process.cwd() + '/challenges',
};

export async function promptForMissingUserState(
userState: UserState
): Promise<UserState> {
userState: IUser
): Promise<IUser> {
const userDevice = getDevice();
let identifier = userState.address;

Expand Down Expand Up @@ -58,9 +56,9 @@ export async function promptForMissingUserState(
user = await upsertUser(user);
}

const { address, ens, installLocations } = user;
const { address, ens, installLocations, challenges, creationTimestamp } = user;
const thisDeviceLocation = installLocations.find((loc: {location: string, device: string}) => loc.device === userDevice);
const newState = { address, ens, installLocation: thisDeviceLocation.location };
const newState = { address, ens, installLocation: thisDeviceLocation.location, challenges, creationTimestamp };
if (JSON.stringify(userState) !== JSON.stringify(newState)) {
// Save the new state locally
await saveUserState(newState);
Expand Down
35 changes: 16 additions & 19 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,21 @@ export type TemplateDescriptor = {
source: string;
};

export type UserState = {
address?: `0x${string}`;
installLocation: string;
lastCompletedChallenge?: string;
lastTreeNode?: string;
export interface IUserChallenge {
challengeName: string
status: string;
lastFeedback: string;
timestamp: Date;
contractAddress: string;
network: string;
gasReport?: IGasReport[];
}

export interface IGasReport {
functionName: string;
gasUsed: number;
}

export interface IChallenge {
type: string;
level: number;
Expand All @@ -102,22 +111,10 @@ export interface IChallenge {
description: string;
}

export interface IUserChallenge {
status: string;
lastFeedback: string;
timestamp: number;
contractAddress: string;
network: string;
gasReport?: {
[key: string]: number;
};
}

export interface IUser {
address: string;
ens: string;
installLocation: string;
creationTimestamp: number;
challenges: {
[key: string]: IUserChallenge;
};
challenges: IUserChallenge[];
}
12 changes: 6 additions & 6 deletions src/utils/stateManager.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import fs from 'fs';
import { promisify } from 'util';
import path from 'path';
import { IChallenge, UserState } from '../types';
import { IChallenge, IUser } from '../types';

const writeFile = promisify(fs.writeFile);

export async function saveUserState(state: UserState) {
export async function saveUserState(state: IUser) {
const configPath = path.join(process.cwd(), "storage");
if (!fs.existsSync(configPath)) {
fs.mkdirSync(configPath);
Expand All @@ -14,14 +14,14 @@ export async function saveUserState(state: UserState) {
await writeFile(filePath, JSON.stringify(state, null, 2));
}

export function loadUserState(): UserState {
export function loadUserState(): IUser {
try {
const configPath = path.join(process.cwd(), "storage", `user.json`);
const data = fs.readFileSync(configPath, 'utf8');
return JSON.parse(data) as UserState;
return JSON.parse(data) as IUser;
} catch (error: any) {
if (error.code === 'ENOENT') {
return {} as UserState; // Return empty object if file doesn't exist
return {} as IUser; // Return empty object if file doesn't exist
}
throw error;
}
Expand All @@ -48,4 +48,4 @@ export function loadChallenges(): IChallenge[] {
}
throw error;
}
}
}
60 changes: 40 additions & 20 deletions src/utils/tree.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import inquirer from "inquirer";
import chalk from "chalk";
import { loadChallenges, loadUserState } from "./stateManager";
import { loadChallenges, loadUserState, saveUserState } from "./stateManager";
import { testChallenge, submitChallenge, setupChallenge } from "../actions";
import { IChallenge } from "../types";
import { IChallenge, IUserChallenge } from "../types";
import fs from "fs";
import { pressEnterToContinue } from "./helpers";
import { getUser } from "../modules/api";

type Action = {
label: string;
Expand All @@ -18,27 +19,31 @@ type TreeNode = {
type: "header" | "challenge" | "quiz" | "capstone-project";
completed?: boolean;
level?: number;
unlocked?: boolean;
actions?: Action[];
repo?: string;
message?: string;
recursive?: boolean;
}

function getNodeLabel(node: TreeNode, depth: string = ""): string {
const { label, level, type, completed } = node;
const { label, level, type, completed, unlocked } = node;
const isHeader = type === "header";
const isChallenge = type === "challenge";
const isQuiz = type === "quiz";
const isCapstoneProject = type === "capstone-project";



if (isHeader) {
return `${depth} ${chalk.blue(label)}`;
} else if (!unlocked) {
return `${depth} ${label} 🔒`;
} else if (isChallenge) {
return `${depth} ${label} ♟️ - LVL ${level}`;
return `${depth} ${label} ${completed ? "🏆" : "🗝️"}`;
} else if (isQuiz) {
return `${depth} ${label} 📖 - LVL ${level}`;
return `${depth} ${label} 📜`;
} else if (isCapstoneProject) {
return`${depth} ${label} 🏆 - LVL ${level}`;
return`${depth} ${label} 💻`;
} else {
return `${depth} ${label}`;
}
Expand All @@ -54,17 +59,21 @@ async function selectNode(node: TreeNode): Promise<void> {
// download repository - Use create-eth to download repository using extensions
// - Show instructions for completing the challenge including a simple command to test their code
// submit project, check if project passes tests then send proof of completion to the BG server, if it passes, mark the challenge as completed
if (node.type === "challenge") {
if (node.type !== "header" && !node.unlocked) {
console.log("This challenge is locked because it doesn't exist yet... 😅");
await pressEnterToContinue();
console.clear();
await startVisualization(header);
} else if (node.type === "challenge") {
const backAction: Action = {
label: "",
label: "⤴️",
action: async () => {
console.clear();
await startVisualization(header);
}
}
const actions = [backAction].concat((node.actions as Action[]).map(action => action));
const choices = actions.map(action => action.label);
console.log("This is a challenge");
const actionPrompt = {
type: "list",
name: "selectedAction",
Expand All @@ -85,7 +94,6 @@ async function selectNode(node: TreeNode): Promise<void> {

// IF: type === personal-challenge
// Show description of challenge


}

Expand Down Expand Up @@ -123,7 +131,8 @@ export async function startVisualization(currentNode?: TreeNode): Promise<void>
depth = depth.replace(/└─/g, " ");
}
// Add spaces so that the labels are spaced out
depth += Array(Math.floor(node.label.length/2)).fill(" ").join("");
const depthDivisor = node.type === "header" ? 5 : 2;
depth += Array(Math.floor(node.label.length / depthDivisor)).fill(" ").join("");
node.children.forEach((child, i, siblings) => getChoicesAndActionsRecursive(child, i === siblings.length - 1, depth));
};

Expand All @@ -137,15 +146,15 @@ export async function startVisualization(currentNode?: TreeNode): Promise<void>
let defaultChoice = 0;
// Add a back option if not at the root
if (parent) {
choices.unshift(" ");
choices.unshift(" ⤴️");
actions.unshift(parent);
defaultChoice = 1;
}
const directionsPrompt = {
type: "list",
loop: false,
name: "selectedNodeIndex",
message: "Select an option",
message: parent ? "Select a challenge" : "Select a category",
choices,
default: defaultChoice
};
Expand Down Expand Up @@ -197,18 +206,24 @@ function NestingMagic(challenges: any[], parentName: string | undefined = undefi
}

export function buildTree(): TreeNode {
const { installLocation } = loadUserState();
const userState = loadUserState();
const { address, installLocation, challenges: userChallenges } = userState;
const tree: TreeNode[] = [];
const challenges = loadChallenges();
const tags = challenges.reduce((acc: string[], challenge: any) => {
return Array.from(new Set(acc.concat(challenge.tags)));
}, []);

for (let tag of tags) {
const filteredChallenges = challenges.filter((challenge: IChallenge) => challenge.tags.includes(tag) && challenge.enabled);
const filteredChallenges = challenges.filter((challenge: IChallenge) => challenge.tags.includes(tag));
let completedCount = 0;
const transformedChallenges = filteredChallenges.map((challenge: IChallenge) => {
const { label, name, level, type, repo, childrenNames} = challenge;
const { label, name, level, type, repo, childrenNames, enabled: unlocked } = challenge;
const parentName = challenges.find((c: any) => c.childrenNames?.includes(name))?.name;

const completed = userChallenges.find((c: IUserChallenge) => c.challengeName === name)?.status === "success";
if (completed) {
completedCount++;
}
// Build selection actions
const actions: Action[] = [];
if (type === "challenge") {
Expand Down Expand Up @@ -247,6 +262,11 @@ export function buildTree(): TreeNode {
console.clear();
// Submit the challenge
await submitChallenge(name);
// Fetch users challenge state from the server
const newUserState = await getUser(address);
userState.challenges = newUserState.challenges;
// Save the new user state locally
await saveUserState(userState);
// Rebuild the tree
globalTree = buildTree();
// Wait for enter key
Expand All @@ -273,13 +293,13 @@ export function buildTree(): TreeNode {
});
}

return { label, name, level, type, actions, childrenNames, parentName };
return { label, name, level, type, actions, completed, childrenNames, parentName, unlocked };
});
const NestingChallenges = NestingMagic(transformedChallenges);

tree.push({
type: "header",
label: `${tag}`,
label: `${tag} ${chalk.green(`(${completedCount}/${filteredChallenges.length})`)}`,
name: `${tag.toLowerCase()}`,
children: NestingChallenges,
recursive: true
Expand Down

0 comments on commit 7222b39

Please sign in to comment.