From 506af2cc33532f52ef6cef2bd35f30d6c2647538 Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Fri, 6 Dec 2024 18:13:38 +0100 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20Make=20the=20connection=20with?= =?UTF-8?q?=20the=20new=20gateway?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/controller/src/iframe/profile.ts | 8 +- .../src/components/context/connection.tsx | 6 + packages/profile/src/hooks/achievements.ts | 261 +++++++++--------- packages/profile/src/hooks/progressions.ts | 54 ++++ packages/profile/src/hooks/trophies.ts | 52 ++++ packages/profile/src/models/index.ts | 28 +- packages/profile/src/models/progress.ts | 73 +++-- packages/profile/src/models/reader.ts | 45 --- packages/profile/src/models/trophy.ts | 122 ++------ .../src/api/cartridge/achievements.graphql | 50 ++++ 10 files changed, 383 insertions(+), 316 deletions(-) create mode 100644 packages/profile/src/hooks/progressions.ts create mode 100644 packages/profile/src/hooks/trophies.ts delete mode 100644 packages/profile/src/models/reader.ts create mode 100644 packages/utils/src/api/cartridge/achievements.graphql diff --git a/packages/controller/src/iframe/profile.ts b/packages/controller/src/iframe/profile.ts index a484fec67..dc8b6e739 100644 --- a/packages/controller/src/iframe/profile.ts +++ b/packages/controller/src/iframe/profile.ts @@ -24,10 +24,12 @@ export class ProfileIFrame extends IFrame { const _url = new URL( slot ? namespace - ? `${_profileUrl}/account/${username}/slot/${slot}?ns=${encodeURIComponent( - namespace, + ? `${_profileUrl}/account/${username}/slot/${slot}?ps=${encodeURIComponent( + slot, + )}&ns=${encodeURIComponent(namespace)}` + : `${_profileUrl}/account/${username}/slot/${slot}?ps=${encodeURIComponent( + slot, )}` - : `${_profileUrl}/account/${username}/slot/${slot}` : `${_profileUrl}/account/${username}`, ); diff --git a/packages/profile/src/components/context/connection.tsx b/packages/profile/src/components/context/connection.tsx index 7d6e01227..9e8d2f871 100644 --- a/packages/profile/src/components/context/connection.tsx +++ b/packages/profile/src/components/context/connection.tsx @@ -20,6 +20,7 @@ type ConnectionContextType = { provider: RpcProvider; chainId: string; erc20: string[]; + project?: string; namespace?: string; isVisible: boolean; setIsVisible: (isVisible: boolean) => void; @@ -63,6 +64,11 @@ export function ConnectionProvider({ children }: { children: ReactNode }) { }); } + const psParam = searchParams.get("ps"); + if (psParam && !state.project) { + state.project = decodeURIComponent(psParam); + } + const nsParam = searchParams.get("ns"); if (nsParam && !state.namespace) { state.namespace = decodeURIComponent(nsParam); diff --git a/packages/profile/src/hooks/achievements.ts b/packages/profile/src/hooks/achievements.ts index b15135b00..65e27a038 100644 --- a/packages/profile/src/hooks/achievements.ts +++ b/packages/profile/src/hooks/achievements.ts @@ -1,13 +1,11 @@ import { useEffect, useMemo, useState } from "react"; import { TROPHY, PROGRESS } from "@/constants"; -import { useEvents } from "./events"; -import { Trophy, Progress } from "@/models"; -import { Task } from "@/components/achievements/trophy"; +import { Trophy, Progress, Task } from "@/models"; import { useConnection } from "./context"; import { useAccount } from "./account"; - -// Number of events to fetch at a time, could be increased if needed -const LIMIT = 1000; +import { useProgressions } from "./progressions"; +import { useTrophies } from "./trophies"; +import { Task as ItemTask } from "@/components/achievements/trophy"; export interface Item { id: string; @@ -22,7 +20,7 @@ export interface Item { percentage: string; completed: boolean; pinned: boolean; - tasks: Task[]; + tasks: ItemTask[]; } export interface Counters { @@ -41,7 +39,7 @@ export interface Player { } export function useAchievements(accountAddress?: string) { - const { namespace } = useConnection(); + const { project, namespace } = useConnection(); const { address } = useAccount(); const [isLoading, setIsLoading] = useState(true); @@ -52,158 +50,159 @@ export function useAchievements(accountAddress?: string) { return accountAddress || address; }, [accountAddress, address]); - const { events: trophies, isFetching: isFetchingTrophiess } = - useEvents({ - namespace: namespace || "", + const { trophies: rawTrophies, isFetching: isFetchingTrophies } = useTrophies( + { + namespace: namespace ?? "", name: TROPHY, - limit: LIMIT, - parse: Trophy.parse, - }); - const { events: progresses, isFetching: isFetchingProgresses } = - useEvents({ - namespace: namespace || "", + project: project ?? "", + parser: Trophy.parse, + }, + ); + + const { progressions: rawProgressions, isFetching: isFetchingProgressions } = + useProgressions({ + namespace: namespace ?? "", name: PROGRESS, - limit: LIMIT, - parse: Progress.parse, + project: project ?? "", + parser: Progress.parse, }); // Compute achievements and players useEffect(() => { if ( - isFetchingTrophiess || - isFetchingProgresses || - !trophies.length || + isFetchingTrophies || + isFetchingProgressions || + !rawTrophies.length || !currentAddress ) return; - // Compute counters - const counters: Counters = {}; - progresses.forEach(({ player, task, count, timestamp }) => { - counters[player] = counters[player] || {}; - counters[player][task] = counters[player][task] || []; - counters[player][task].push({ count, timestamp }); + // Merge trophies + const trophies: { [id: string]: Trophy } = {}; + rawTrophies.forEach((trophy) => { + if (Object.keys(trophies).includes(trophy.id)) { + trophy.tasks.forEach((task) => { + if (!trophies[trophy.id].tasks.find((t) => t.id === task.id)) { + trophies[trophy.id].tasks.push(task); + } + }); + } else { + trophies[trophy.id] = trophy; + } }); // Compute players and achievement stats - const dedupedTrophies = trophies.filter( - (trophy, index) => - trophies.findIndex((t) => t.id === trophy.id) === index, - ); + const data: { + [playerId: string]: { + [achievementId: string]: { + [taskId: string]: { + completion: boolean; + timestamp: number; + count: number; + }; + }; + }; + } = {}; + rawProgressions.forEach((progress: Progress) => { + const { achievementId, playerId, taskId, taskTotal, total, timestamp } = + progress; + + // Compute player + const detaultTasks: { [taskId: string]: boolean } = {}; + trophies[achievementId].tasks.forEach((task: Task) => { + detaultTasks[task.id] = false; + }); + data[playerId] = data[playerId] || {}; + data[playerId][achievementId] = + data[playerId][achievementId] || detaultTasks; + data[playerId][achievementId][taskId] = { + completion: total >= taskTotal, + timestamp, + count: total, + }; + }); + const stats: Stats = {}; - const players: Player[] = Object.keys(counters) - .map((playerAddress) => { - let timestamp = 0; - const completeds: string[] = []; - const earnings = dedupedTrophies.reduce( - (total: number, trophy: Trophy) => { - // Compute at which timestamp the latest achievement was completed - let completed = true; - trophy.tasks.forEach((task) => { - let count = 0; - let completion = false; - counters[playerAddress]?.[task.id] - ?.sort((a, b) => a.timestamp - b.timestamp) - .forEach( - ({ - count: c, - timestamp: t, - }: { - count: number; - timestamp: number; - }) => { - count += c; - if (!completion && count >= task.total) { - timestamp = t > timestamp ? t : timestamp; - completion = true; - } - }, - ); - completed = completed && completion; - }); - // Add trophy to list if completed - if (completed) completeds.push(trophy.id); - // Update stats - stats[trophy.id] = stats[trophy.id] || 0; - stats[trophy.id] += completed ? 1 : 0; - return completed ? total + trophy.earning : total; - }, - 0, + const players: Player[] = []; + Object.keys(data).forEach((playerId) => { + const player = data[playerId]; + const completeds: string[] = []; + let timestamp = 0; + const earnings = Object.keys(player).reduce((acc, achievementId) => { + const completion = Object.values(player[achievementId]).every( + (task) => task.completion, ); - return { - address: playerAddress, - earnings, - timestamp, - completeds, - }; - }) - .sort((a, b) => a.timestamp - b.timestamp) // Oldest to newest - .sort((a, b) => b.earnings - a.earnings); // Highest to lowest - setPlayers(players); + timestamp = Math.max( + ...Object.values(player[achievementId]).map((task) => task.timestamp), + ); + if (completion) { + completeds.push(achievementId); + stats[achievementId] = stats[achievementId] || 0; + stats[achievementId] += 1; + } + return acc + (completion ? trophies[achievementId].earning : 0); + }, 0); + players.push({ + address: playerId, + earnings, + timestamp: timestamp, + completeds, + }); + }); + + setPlayers( + Object.values(players) + .sort((a, b) => a.timestamp - b.timestamp) // Oldest to newest + .sort((a, b) => b.earnings - a.earnings), // Highest to lowest + ); // Compute achievements - const achievements: Item[] = dedupedTrophies - .map((trophy) => { - // Compute at which timestamp the achievement was completed - let timestamp = 0; - let completed = true; - const tasks: Task[] = []; - trophy.tasks.forEach((task) => { - let count = 0; - let completion = false; - counters[currentAddress]?.[task.id] - ?.sort((a, b) => a.timestamp - b.timestamp) - .forEach( - ({ - count: c, - timestamp: t, - }: { - count: number; - timestamp: number; - }) => { - count += c; - if (!completion && count >= task.total) { - timestamp = t; - completion = true; - } - }, - ); - tasks.push({ - id: task.id, - count, - total: task.total, - description: task.description, - }); - completed = completed && completion; - }); + const achievements: Item[] = Object.values(trophies).map( + (trophy: Trophy) => { + const achievement = data[currentAddress]?.[trophy.id] || {}; + const completion = + Object.values(achievement).every((task) => task.completion) || false; + const timestamp = Math.max( + ...Object.values(achievement).map((task) => task.timestamp), + ); // Compute percentage of players who completed the achievement const percentage = ( - players.length ? (100 * stats[trophy.id]) / players.length : 0 + players.length ? (100 * (stats[trophy.id] ?? 0)) / players.length : 0 ).toFixed(0); + const tasks: ItemTask[] = trophy.tasks.map((task) => { + return { + ...task, + count: achievement[task.id]?.count || 0, + }; + }); return { - ...trophy, - completed, + id: trophy.id, + hidden: trophy.hidden, + index: trophy.index, + earning: trophy.earning, + group: trophy.group, + icon: trophy.icon, + title: trophy.title, + description: trophy.description, + completed: completion, percentage, timestamp, pinned: false, tasks, }; - }) - .sort((a, b) => a.index - b.index) // Lowest index to greatest - .sort((a, b) => (a.id > b.id ? 1 : -1)) // A to Z - .sort((a, b) => (b.hidden ? -1 : 1) - (a.hidden ? -1 : 1)) // Visible to hidden - .sort((a, b) => b.timestamp - a.timestamp) // Newest to oldest - .sort((a, b) => (b.completed ? 1 : 0) - (a.completed ? 1 : 0)); // Completed to uncompleted - setAchievements(achievements); + }, + ); + setAchievements( + achievements + .sort((a, b) => a.index - b.index) // Lowest index to greatest + .sort((a, b) => (a.id > b.id ? 1 : -1)) // A to Z + .sort((a, b) => (b.hidden ? -1 : 1) - (a.hidden ? -1 : 1)) // Visible to hidden + .sort((a, b) => b.timestamp - a.timestamp) // Newest to oldest + .sort((a, b) => (b.completed ? 1 : 0) - (a.completed ? 1 : 0)), // Completed to uncompleted + ); // Update loading state setIsLoading(false); - }, [ - currentAddress, - trophies, - progresses, - isFetchingTrophiess, - isFetchingProgresses, - ]); + }, [currentAddress, isFetchingTrophies, isFetchingProgressions]); return { achievements, players, isLoading }; } diff --git a/packages/profile/src/hooks/progressions.ts b/packages/profile/src/hooks/progressions.ts new file mode 100644 index 000000000..e4abedfe8 --- /dev/null +++ b/packages/profile/src/hooks/progressions.ts @@ -0,0 +1,54 @@ +import { useEffect, useState } from "react"; +import { Project, useProgressionsQuery } from "@cartridge/utils/api/cartridge"; +import { Progress, getSelectorFromTag } from "@/models"; + +export function useProgressions({ + namespace, + name, + project, + parser, +}: { + namespace: string; + name: string; + project: string; + parser: (node: any) => Progress; +}) { + const [progressions, setProgressions] = useState<{ [key: string]: Progress }>( + {}, + ); + + // Fetch achievement creations from raw events + const projects: Project[] = [ + { model: getSelectorFromTag(namespace, name), namespace, project }, + ]; + const { refetch: fetchProgressions, isFetching } = useProgressionsQuery( + { + projects, + }, + { + enabled: !!namespace && !!project, + refetchInterval: 30_000, // Refetch every 30 seconds + onSuccess: ({ playerAchievements }: { playerAchievements: any }) => { + const progressions = playerAchievements.items[0].achievements + .map(parser) + .reduce((acc: { [key: string]: Progress }, achievement: Progress) => { + acc[achievement.key] = achievement; + return acc; + }, {}); + setProgressions((previous) => ({ ...previous, ...progressions })); + }, + }, + ); + + useEffect(() => { + if (!namespace || !project) return; + try { + fetchProgressions(); + } catch (error) { + // Could happen if the indexer is down or wrong url + console.error(error); + } + }, [namespace, project, fetchProgressions]); + + return { progressions: Object.values(progressions), isFetching }; +} diff --git a/packages/profile/src/hooks/trophies.ts b/packages/profile/src/hooks/trophies.ts new file mode 100644 index 000000000..40612e802 --- /dev/null +++ b/packages/profile/src/hooks/trophies.ts @@ -0,0 +1,52 @@ +import { useEffect, useState } from "react"; +import { Project, useAchievementsQuery } from "@cartridge/utils/api/cartridge"; +import { Trophy, getSelectorFromTag } from "@/models"; + +export function useTrophies({ + namespace, + name, + project, + parser, +}: { + namespace: string; + name: string; + project: string; + parser: (node: any) => Trophy; +}) { + const [trophies, setTrophies] = useState<{ [key: string]: Trophy }>({}); + + // Fetch achievement creations from raw events + const projects: Project[] = [ + { model: getSelectorFromTag(namespace, name), namespace, project }, + ]; + const { refetch: fetchAchievements, isFetching } = useAchievementsQuery( + { + projects, + }, + { + enabled: !!namespace && !!project, + refetchInterval: 300_000, // Refetch every 5 minutes + onSuccess: ({ achievements }: { achievements: any }) => { + const trophies = achievements.items[0].achievements + .map(parser) + .reduce((acc: { [key: string]: Trophy }, achievement: Trophy) => { + acc[achievement.key] = achievement; + return acc; + }, {}); + setTrophies((previous) => ({ ...trophies, ...previous })); + }, + }, + ); + + useEffect(() => { + if (!namespace || !project) return; + try { + fetchAchievements(); + } catch (error) { + // Could happen if the indexer is down or wrong url + console.error(error); + } + }, [namespace, project, fetchAchievements]); + + return { trophies: Object.values(trophies), isFetching }; +} diff --git a/packages/profile/src/models/index.ts b/packages/profile/src/models/index.ts index 2c4d8ed1f..2212bf9c1 100644 --- a/packages/profile/src/models/index.ts +++ b/packages/profile/src/models/index.ts @@ -1,3 +1,29 @@ -export * from "./reader"; +import { ByteArray, byteArray, hash } from "starknet"; + export * from "./trophy"; export * from "./progress"; + +// Computes dojo selector from namespace and event name +export function getSelectorFromTag(namespace: string, event: string): string { + return hash.computePoseidonHashOnElements([ + computeByteArrayHash(namespace), + computeByteArrayHash(event), + ]); +} + +// Poseidon hash of a string representated as a ByteArray +export function computeByteArrayHash(str: string): string { + const bytes = byteArray.byteArrayFromString(str); + return hash.computePoseidonHashOnElements(serializeByteArray(bytes)); +} + +// Serializes a ByteArray to a bigint array +export function serializeByteArray(byteArray: ByteArray): bigint[] { + const result: bigint[] = [ + BigInt(byteArray.data.length), + ...byteArray.data.map((word) => BigInt(word.toString())), + BigInt(byteArray.pending_word), + BigInt(byteArray.pending_word_len), + ]; + return result; +} diff --git a/packages/profile/src/models/progress.ts b/packages/profile/src/models/progress.ts index f37139114..0edb83315 100644 --- a/packages/profile/src/models/progress.ts +++ b/packages/profile/src/models/progress.ts @@ -1,52 +1,47 @@ -import { EventNode } from "@cartridge/utils/api/indexer"; -import { Reader } from "./reader"; - export class Progress { - player: string; - task: string; - count: number; + key: string; + achievementId: string; + playerId: string; + points: number; + taskId: string; + taskTotal: number; + total: number; timestamp: number; - constructor(player: string, task: string, count: number, timestamp: number) { - this.player = player; - this.task = task; - this.count = count; + constructor( + key: string, + achievementId: string, + playerId: string, + points: number, + taskId: string, + taskTotal: number, + total: number, + timestamp: number, + ) { + this.key = key; + this.achievementId = achievementId; + this.playerId = playerId; + this.points = points; + this.taskId = taskId; + this.taskTotal = taskTotal; + this.total = total; this.timestamp = timestamp; } - static from(node: EventNode): Progress { + static from(node: any): Progress { return Progress.parse(node); } - static parse(node: EventNode): Progress { - const reader = new ProgressReader(node); + static parse(node: any): Progress { return { - player: reader.popPlayer(), - task: reader.popTask(), - count: reader.popCount(), - timestamp: reader.popTimestamp(), + key: `${node.playerId}-${node.achievementId}-${node.taskId}`, + achievementId: node.achievementId, + playerId: node.playerId, + points: node.points, + taskId: node.taskId, + taskTotal: node.taskTotal, + total: node.total, + timestamp: new Date(node.completionTime).getTime() / 1000, }; } } - -class ProgressReader extends Reader { - constructor(node: EventNode) { - super(node); - } - - popPlayer(): string { - return this.popValue(); - } - - popTask(): string { - return this.popString(2); - } - - popCount(): number { - return this.popNumber(); - } - - popTimestamp(): number { - return this.popNumber(); - } -} diff --git a/packages/profile/src/models/reader.ts b/packages/profile/src/models/reader.ts deleted file mode 100644 index 3933b4db8..000000000 --- a/packages/profile/src/models/reader.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { EventNode } from "@cartridge/utils/api/indexer"; -import { byteArray, shortString } from "starknet"; - -export class Reader { - node: EventNode; - cursor: number; - - constructor(node: EventNode) { - this.node = node; - this.cursor = 1; - } - - popValue(shift = 1): string { - const value = this.node.data[this.cursor]; - this.cursor += shift; - return value; - } - - popNumber(shift = 1): number { - return parseInt(this.popValue(shift)); - } - - popBoolean(shift = 1): boolean { - const value = this.popValue(shift); - return parseInt(value) ? false : true; - } - - popString(shift = 1): string { - const value = this.popValue(shift); - return shortString.decodeShortString(value); - } - - popByteArray(shift = 1): string { - const length = this.popNumber(); - const value = byteArray.stringFromByteArray({ - data: length - ? this.node.data.slice(this.cursor, this.cursor + length) - : [], - pending_word: this.node.data[this.cursor + length], - pending_word_len: this.node.data[this.cursor + length + 1], - }); - this.cursor += length + 1 + shift; - return value; - } -} diff --git a/packages/profile/src/models/trophy.ts b/packages/profile/src/models/trophy.ts index b47daac2c..112d634d1 100644 --- a/packages/profile/src/models/trophy.ts +++ b/packages/profile/src/models/trophy.ts @@ -1,5 +1,4 @@ -import { EventNode } from "@cartridge/utils/api/indexer"; -import { Reader } from "."; +import { shortString } from "starknet"; export interface Task { id: string; @@ -8,6 +7,7 @@ export interface Task { } export class Trophy { + key: string; id: string; hidden: boolean; index: number; @@ -22,6 +22,7 @@ export class Trophy { data: string; constructor( + key: string, id: string, hidden: boolean, index: number, @@ -35,6 +36,7 @@ export class Trophy { tasks: Task[], data: string, ) { + this.key = key; this.id = id; this.hidden = hidden; this.index = index; @@ -49,105 +51,31 @@ export class Trophy { this.data = data; } - static from(node: EventNode): Trophy { + static from(node: any): Trophy { return Trophy.parse(node); } - static parse(node: EventNode): Trophy { - try { - return Trophy.parseV0(node); - } catch { - return Trophy.parseV1(node); - } - } - - static parseV1(node: EventNode): Trophy { - const reader = new TrophyReader(node); - return { - id: reader.popId(), - hidden: reader.popHidden(), - index: reader.popIndex(), - earning: reader.popEarning(), - start: reader.popNumber(), - end: reader.popNumber(), - group: reader.popString(), - icon: reader.popString(), - title: reader.popString(), - description: reader.popByteArray(), - tasks: reader.popTasks(), - data: "", - }; - } - - static parseV0(node: EventNode): Trophy { - const reader = new TrophyReader(node); + static parse(node: any): Trophy { return { - id: reader.popId(), - hidden: reader.popHidden(), - index: reader.popIndex(), - earning: reader.popEarning(), - start: 0, - end: 0, - group: reader.popGroup(), - icon: reader.popIcon(), - title: reader.popTitle(), - description: reader.popDescription(), - tasks: reader.popTasks(), - data: "", + key: `${node.id}-${node.taskId}`, + id: node.id, + hidden: node.hidden === 1, + index: node.page, + earning: node.points, + start: node.start === "0x" ? 0 : node.start, + end: node.end === "0x" ? 0 : node.end, + group: shortString.decodeShortString(node.achievementGroup), + icon: shortString.decodeShortString(node.icon), + title: shortString.decodeShortString(node.title), + description: node.description, + tasks: [ + { + id: node.taskId, + total: node.taskTotal, + description: node.taskDescription, + }, + ], + data: node.data, }; } } - -class TrophyReader extends Reader { - constructor(node: EventNode) { - super(node); - } - - popId(): string { - return this.popString(2); - } - - popHidden(): boolean { - return !this.popBoolean(); - } - - popIndex(): number { - return this.popNumber(); - } - - popEarning(): number { - return this.popNumber(); - } - - popGroup(): string { - return this.popString(); - } - - popIcon(): string { - return this.popString(); - } - - popTitle(): string { - return this.popString(); - } - - popDescription(): string { - return this.popByteArray(); - } - - popTasks(): Task[] { - const tasks = []; - const length = this.popNumber(); - for (let i = 0; i < length; i++) { - tasks.push(this.popTask()); - } - return tasks; - } - - popTask(): Task { - const id = this.popString(); - const total = this.popNumber(); - const description = this.popDescription(); - return { id, total, description }; - } -} diff --git a/packages/utils/src/api/cartridge/achievements.graphql b/packages/utils/src/api/cartridge/achievements.graphql new file mode 100644 index 000000000..4f80a705a --- /dev/null +++ b/packages/utils/src/api/cartridge/achievements.graphql @@ -0,0 +1,50 @@ +query Achievements($projects: [Project!]!) { + achievements(projects: $projects) { + items { + meta { + project + model + namespace + count + } + achievements { + id + hidden + page + points + start + end + achievementGroup + icon + title + description + taskId + taskTotal + taskDescription + data + } + } + } +} + +query Progressions($projects: [Project!]!) { + playerAchievements(projects: $projects) { + items { + meta { + project + model + namespace + count + } + achievements { + playerId + achievementId + points + taskId + taskTotal + total + completionTime + } + } + } +} From 061baad01f8d1c6385d067243c820b6035276f9a Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Fri, 6 Dec 2024 18:29:48 +0100 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=90=9B=20Add=20response=20interfaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/profile/src/hooks/progressions.ts | 8 +++++-- packages/profile/src/hooks/trophies.ts | 8 +++++-- packages/profile/src/models/progress.ts | 10 +++++++++ packages/profile/src/models/trophy.ts | 25 ++++++++++++++++++---- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/packages/profile/src/hooks/progressions.ts b/packages/profile/src/hooks/progressions.ts index e4abedfe8..de6f22fbf 100644 --- a/packages/profile/src/hooks/progressions.ts +++ b/packages/profile/src/hooks/progressions.ts @@ -1,6 +1,10 @@ import { useEffect, useState } from "react"; import { Project, useProgressionsQuery } from "@cartridge/utils/api/cartridge"; -import { Progress, getSelectorFromTag } from "@/models"; +import { Progress, RawProgress, getSelectorFromTag } from "@/models"; + +interface Response { + items: { achievements: RawProgress[] }[]; +} export function useProgressions({ namespace, @@ -28,7 +32,7 @@ export function useProgressions({ { enabled: !!namespace && !!project, refetchInterval: 30_000, // Refetch every 30 seconds - onSuccess: ({ playerAchievements }: { playerAchievements: any }) => { + onSuccess: ({ playerAchievements }: { playerAchievements: Response }) => { const progressions = playerAchievements.items[0].achievements .map(parser) .reduce((acc: { [key: string]: Progress }, achievement: Progress) => { diff --git a/packages/profile/src/hooks/trophies.ts b/packages/profile/src/hooks/trophies.ts index 40612e802..75780f167 100644 --- a/packages/profile/src/hooks/trophies.ts +++ b/packages/profile/src/hooks/trophies.ts @@ -1,6 +1,10 @@ import { useEffect, useState } from "react"; import { Project, useAchievementsQuery } from "@cartridge/utils/api/cartridge"; -import { Trophy, getSelectorFromTag } from "@/models"; +import { RawTrophy, Trophy, getSelectorFromTag } from "@/models"; + +interface Response { + items: { achievements: RawTrophy[] }[]; +} export function useTrophies({ namespace, @@ -26,7 +30,7 @@ export function useTrophies({ { enabled: !!namespace && !!project, refetchInterval: 300_000, // Refetch every 5 minutes - onSuccess: ({ achievements }: { achievements: any }) => { + onSuccess: ({ achievements }: { achievements: Response }) => { const trophies = achievements.items[0].achievements .map(parser) .reduce((acc: { [key: string]: Trophy }, achievement: Trophy) => { diff --git a/packages/profile/src/models/progress.ts b/packages/profile/src/models/progress.ts index 0edb83315..46ac8af10 100644 --- a/packages/profile/src/models/progress.ts +++ b/packages/profile/src/models/progress.ts @@ -1,3 +1,13 @@ +export interface RawProgress { + achievementId: string; + playerId: string; + points: number; + taskId: string; + taskTotal: number; + total: number; + timestamp: number; +} + export class Progress { key: string; achievementId: string; diff --git a/packages/profile/src/models/trophy.ts b/packages/profile/src/models/trophy.ts index 112d634d1..1cc866ecf 100644 --- a/packages/profile/src/models/trophy.ts +++ b/packages/profile/src/models/trophy.ts @@ -1,5 +1,22 @@ import { shortString } from "starknet"; +export interface RawTrophy { + id: string; + hidden: number; + page: number; + points: number; + start: string; + end: string; + achievementGroup: string; + icon: string; + title: string; + description: string; + taskId: string; + taskTotal: number; + taskDescription: string; + data: string; +} + export interface Task { id: string; total: number; @@ -51,19 +68,19 @@ export class Trophy { this.data = data; } - static from(node: any): Trophy { + static from(node: RawTrophy): Trophy { return Trophy.parse(node); } - static parse(node: any): Trophy { + static parse(node: RawTrophy): Trophy { return { key: `${node.id}-${node.taskId}`, id: node.id, hidden: node.hidden === 1, index: node.page, earning: node.points, - start: node.start === "0x" ? 0 : node.start, - end: node.end === "0x" ? 0 : node.end, + start: node.start === "0x" ? 0 : parseInt(node.start), + end: node.end === "0x" ? 0 : parseInt(node.end), group: shortString.decodeShortString(node.achievementGroup), icon: shortString.decodeShortString(node.icon), title: shortString.decodeShortString(node.title), From 1f490500c76cddd0e7c7a6c39d72db175c7b53cc Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Fri, 6 Dec 2024 18:40:27 +0100 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20theme=20in=20localhost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/profile/src/components/context/connection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/profile/src/components/context/connection.tsx b/packages/profile/src/components/context/connection.tsx index 95579f7eb..86c9970d6 100644 --- a/packages/profile/src/components/context/connection.tsx +++ b/packages/profile/src/components/context/connection.tsx @@ -35,7 +35,7 @@ type ParentMethods = { }; const initialState: ConnectionContextType = { - origin: "", + origin: location.origin, parent: { close: () => {}, openSettings: () => {}, From a5a841437aef79e40ea7b77af0280e057565f9c2 Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Fri, 6 Dec 2024 18:48:11 +0100 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20missing=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/profile/src/hooks/progressions.ts | 2 +- packages/profile/src/hooks/trophies.ts | 2 +- packages/profile/src/models/progress.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/profile/src/hooks/progressions.ts b/packages/profile/src/hooks/progressions.ts index de6f22fbf..cec3a1c77 100644 --- a/packages/profile/src/hooks/progressions.ts +++ b/packages/profile/src/hooks/progressions.ts @@ -15,7 +15,7 @@ export function useProgressions({ namespace: string; name: string; project: string; - parser: (node: any) => Progress; + parser: (node: RawProgress) => Progress; }) { const [progressions, setProgressions] = useState<{ [key: string]: Progress }>( {}, diff --git a/packages/profile/src/hooks/trophies.ts b/packages/profile/src/hooks/trophies.ts index 75780f167..4466fdd96 100644 --- a/packages/profile/src/hooks/trophies.ts +++ b/packages/profile/src/hooks/trophies.ts @@ -15,7 +15,7 @@ export function useTrophies({ namespace: string; name: string; project: string; - parser: (node: any) => Trophy; + parser: (node: RawTrophy) => Trophy; }) { const [trophies, setTrophies] = useState<{ [key: string]: Trophy }>({}); diff --git a/packages/profile/src/models/progress.ts b/packages/profile/src/models/progress.ts index 46ac8af10..0c3740781 100644 --- a/packages/profile/src/models/progress.ts +++ b/packages/profile/src/models/progress.ts @@ -5,7 +5,7 @@ export interface RawProgress { taskId: string; taskTotal: number; total: number; - timestamp: number; + completionTime: number; } export class Progress { @@ -38,11 +38,11 @@ export class Progress { this.timestamp = timestamp; } - static from(node: any): Progress { + static from(node: RawProgress): Progress { return Progress.parse(node); } - static parse(node: any): Progress { + static parse(node: RawProgress): Progress { return { key: `${node.playerId}-${node.achievementId}-${node.taskId}`, achievementId: node.achievementId, From 77e36ced8eefc28281baa6aee84b807e16018d77 Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Fri, 6 Dec 2024 19:00:55 +0100 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20achievement=20completi?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/profile/src/hooks/achievements.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/profile/src/hooks/achievements.ts b/packages/profile/src/hooks/achievements.ts index 65e27a038..be51095f8 100644 --- a/packages/profile/src/hooks/achievements.ts +++ b/packages/profile/src/hooks/achievements.ts @@ -161,7 +161,8 @@ export function useAchievements(accountAddress?: string) { (trophy: Trophy) => { const achievement = data[currentAddress]?.[trophy.id] || {}; const completion = - Object.values(achievement).every((task) => task.completion) || false; + Object.values(achievement).length > 0 && + Object.values(achievement).every((task) => task.completion); const timestamp = Math.max( ...Object.values(achievement).map((task) => task.timestamp), ); From 9e622c174768b33c5dfd2937b0d2fa99171c64eb Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Fri, 6 Dec 2024 19:04:25 +0100 Subject: [PATCH 6/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20page=20change=20on=20q?= =?UTF-8?q?uery=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/profile/src/components/achievements/trophies.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/profile/src/components/achievements/trophies.tsx b/packages/profile/src/components/achievements/trophies.tsx index cc4849cd6..fcde67c60 100644 --- a/packages/profile/src/components/achievements/trophies.tsx +++ b/packages/profile/src/components/achievements/trophies.tsx @@ -121,7 +121,7 @@ function Group({ setPages(pages); const page = filtereds.find((a) => !a.completed); setPage(page ? page.index : pages[pages.length - 1]); - }, [items]); + }, []); const handleNext = useCallback(() => { const index = pages.indexOf(page);