Skip to content

Commit

Permalink
✨ Add openAt and openTo and fix trophies to update accordingly (#1304)
Browse files Browse the repository at this point in the history
* ✨ Add openAt and openTo and fix trophies to update accordingly

* 💄 Code lint
  • Loading branch information
bal7hazar authored Jan 21, 2025
1 parent 02407fa commit 5905332
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 94 deletions.
5 changes: 1 addition & 4 deletions examples/next/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
{
"extends": [
"next/core-web-vitals",
"next/typescript"
]
"extends": ["next/core-web-vitals", "next/typescript"]
}
96 changes: 77 additions & 19 deletions examples/next/src/components/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,95 @@
import { useAccount } from "@starknet-react/core";
import ControllerConnector from "@cartridge/connector/controller";
import { Button } from "@cartridge/ui-next";
import { ETH_CONTRACT_ADDRESS } from "./providers/StarknetProvider";
import { useEffect, useState } from "react";

export function Profile() {
const { account, connector } = useAccount();
const [username, setUsername] = useState<string | null>(null);
const ctrlConnector = connector as unknown as ControllerConnector;

useEffect(() => {
async function fetch() {
try {
const name = await (connector as ControllerConnector)?.username();
if (!name) return;
setUsername(name);
} catch (error) {
console.error(error);
}
}
fetch();
}, [connector]);

if (!account) {
return null;
}

return (
<div>
<h2>Open Profile</h2>
<div className="flex gap-1">
<Button onClick={() => ctrlConnector.controller.openProfile()}>
Inventory
</Button>
<Button
onClick={() => ctrlConnector.controller.openProfile("achievements")}
>
Achievements
</Button>
<Button
onClick={() => ctrlConnector.controller.openProfile("trophies")}
>
Trophies
</Button>
<Button
onClick={() => ctrlConnector.controller.openProfile("activity")}
>
Activity
</Button>
<div className="flex flex-col gap-1">
<div className="flex gap-1">
<Button onClick={() => ctrlConnector.controller.openProfile()}>
Inventory
</Button>
<Button
onClick={() => ctrlConnector.controller.openProfile("achievements")}
>
Achievements
</Button>
<Button
onClick={() => ctrlConnector.controller.openProfile("trophies")}
>
Trophies
</Button>
<Button
onClick={() => ctrlConnector.controller.openProfile("activity")}
>
Activity
</Button>
</div>
<div className="flex gap-1">
<Button
onClick={() =>
ctrlConnector.controller.openProfileTo(
`inventory/token/${ETH_CONTRACT_ADDRESS}?preset=cartridge`,
)
}
>
Open to Token ETH
</Button>
<Button
onClick={() =>
ctrlConnector.controller.openProfileTo(
`inventory/token/${ETH_CONTRACT_ADDRESS}/send?preset=cartridge`,
)
}
>
Open to Token ETH Send
</Button>
</div>
<div className="flex gap-1">
<Button
onClick={() =>
ctrlConnector.controller.openProfileAt(
`account/${username}/slot/ryomainnet/achievements?ps=ryomainnet&ns=dopewars&preset=dope-wars`,
)
}
>
Open at Dopewars Achievements
</Button>
<Button
onClick={() =>
ctrlConnector.controller.openProfileAt(
`account/${username}/slot/darkshuffle-mainnet-3/achievements?ps=darkshuffle-mainnet-3&ns=darkshuffle_s0&preset=dark-shuffle`,
)
}
>
Open at Dark Shuffle Achievements
</Button>
</div>
</div>
</div>
);
Expand Down
28 changes: 28 additions & 0 deletions packages/controller/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,34 @@ export default class ControllerProvider extends BaseProvider {
this.iframes.profile.open();
}

async openProfileTo(to: string) {
if (!this.profile || !this.iframes.profile?.url) {
console.error("Profile is not ready");
return;
}
if (!this.account) {
console.error("Account is not ready");
return;
}

this.profile.navigate(`${this.iframes.profile.url?.pathname}/${to}`);
this.iframes.profile.open();
}

async openProfileAt(at: string) {
if (!this.profile || !this.iframes.profile?.url) {
console.error("Profile is not ready");
return;
}
if (!this.account) {
console.error("Account is not ready");
return;
}

this.profile.navigate(at);
this.iframes.profile.open();
}

async openSettings() {
if (!this.keychain || !this.iframes.keychain) {
console.error(new NotReadyToConnect().message);
Expand Down
4 changes: 2 additions & 2 deletions packages/profile/src/components/context/connection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
}

const psParam = searchParams.get("ps");
if (psParam && !state.project) {
if (psParam) {
state.project = decodeURIComponent(psParam);
}

const nsParam = searchParams.get("ns");
if (nsParam && !state.namespace) {
if (nsParam) {
state.namespace = decodeURIComponent(nsParam);
}

Expand Down
79 changes: 18 additions & 61 deletions packages/profile/src/hooks/achievements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,67 +45,28 @@ export function useAchievements(accountAddress?: string) {
const [isLoading, setIsLoading] = useState(true);
const [achievements, setAchievements] = useState<Item[]>([]);
const [players, setPlayers] = useState<Player[]>([]);
const [cachedTrophies, setCachedTrophies] = useState<Trophy[]>([]);
const [cachedProgressions, setCachedProgressions] = useState<Progress[]>([]);

const currentAddress = useMemo(() => {
return accountAddress || address;
}, [accountAddress, address]);

const { trophies: rawTrophies, isFetching: isFetchingTrophies } = useTrophies(
{
namespace: namespace ?? "",
name: TROPHY,
project: project ?? "",
parser: Trophy.parse,
},
);

const { progressions: rawProgressions, isFetching: isFetchingProgressions } =
useProgressions({
namespace: namespace ?? "",
name: PROGRESS,
project: project ?? "",
parser: Progress.parse,
});
const { trophies } = useTrophies({
namespace: namespace ?? "",
name: TROPHY,
project: project ?? "",
parser: Trophy.parse,
});

useEffect(() => {
if (!isFetchingTrophies && cachedTrophies.length !== rawTrophies.length) {
setCachedTrophies(rawTrophies);
}
if (
!isFetchingProgressions &&
Math.max(...rawProgressions.map((p) => p.timestamp)) >
Math.max(...cachedProgressions.map((p) => p.timestamp))
) {
setCachedProgressions(rawProgressions);
}
}, [
rawTrophies,
rawProgressions,
cachedTrophies,
cachedProgressions,
isFetchingTrophies,
isFetchingProgressions,
]);
const { progressions } = useProgressions({
namespace: namespace ?? "",
name: PROGRESS,
project: project ?? "",
parser: Progress.parse,
});

// Compute achievements and players
useEffect(() => {
if (!cachedTrophies.length || !currentAddress) return;

// Merge trophies
const trophies: { [id: string]: Trophy } = {};
cachedTrophies.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;
}
});
if (!Object.values(trophies).length || !currentAddress) return;

// Compute players and achievement stats
const data: {
Expand All @@ -119,13 +80,15 @@ export function useAchievements(accountAddress?: string) {
};
};
} = {};
cachedProgressions.forEach((progress: Progress) => {
Object.values(progressions).forEach((progress: Progress) => {
const { achievementId, playerId, taskId, taskTotal, total, timestamp } =
progress;

// Compute player
const detaultTasks: { [taskId: string]: boolean } = {};
trophies[achievementId].tasks.forEach((task: Task) => {
const trophy = trophies[achievementId];
if (!trophy) return;
trophy.tasks.forEach((task: Task) => {
detaultTasks[task.id] = false;
});
data[playerId] = data[playerId] || {};
Expand Down Expand Up @@ -222,13 +185,7 @@ export function useAchievements(accountAddress?: string) {
);
// Update loading state
setIsLoading(false);
}, [
currentAddress,
isFetchingTrophies,
isFetchingProgressions,
cachedTrophies,
cachedProgressions,
]);
}, [currentAddress, trophies, progressions]);

return { achievements, players, isLoading };
}
23 changes: 17 additions & 6 deletions packages/profile/src/hooks/progressions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Project, useProgressionsQuery } from "@cartridge/utils/api/cartridge";
import { Progress, RawProgress, getSelectorFromTag } from "@/models";

Expand All @@ -17,20 +17,26 @@ export function useProgressions({
project: string;
parser: (node: RawProgress) => Progress;
}) {
const [rawProgressions, setRawProgressions] = useState<{
[key: string]: Progress;
}>({});
const [progressions, setProgressions] = useState<{ [key: string]: Progress }>(
{},
);

// Fetch achievement creations from raw events
const projects: Project[] = [
{ model: getSelectorFromTag(namespace, name), namespace, project },
];
const projects: Project[] = useMemo(
() => [{ model: getSelectorFromTag(namespace, name), namespace, project }],
[namespace, name, project],
);

const { refetch: fetchProgressions, isFetching } = useProgressionsQuery(
{
projects,
},
{
enabled: !!namespace && !!project,
queryKey: ["progressions", namespace, name, project],
refetchInterval: 600_000, // Refetch every 10 minutes
onSuccess: ({ playerAchievements }: { playerAchievements: Response }) => {
const progressions = playerAchievements.items[0].achievements
Expand All @@ -39,7 +45,7 @@ export function useProgressions({
acc[achievement.key] = achievement;
return acc;
}, {});
setProgressions((previous) => ({ ...previous, ...progressions }));
setRawProgressions(progressions);
},
},
);
Expand All @@ -54,5 +60,10 @@ export function useProgressions({
}
}, [namespace, project, fetchProgressions]);

return { progressions: Object.values(progressions), isFetching };
useEffect(() => {
if (isFetching) return;
setProgressions(rawProgressions);
}, [rawProgressions, isFetching]);

return { progressions };
}
24 changes: 22 additions & 2 deletions packages/profile/src/hooks/trophies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function useTrophies({
project: string;
parser: (node: RawTrophy) => Trophy;
}) {
const [rawTrophies, setRawTrophies] = useState<{ [key: string]: Trophy }>({});
const [trophies, setTrophies] = useState<{ [key: string]: Trophy }>({});

// Fetch achievement creations from raw events
Expand All @@ -29,6 +30,7 @@ export function useTrophies({
},
{
enabled: !!namespace && !!project,
queryKey: ["achievements", namespace, name, project],
refetchInterval: 600_000, // Refetch every 10 minutes
onSuccess: ({ achievements }: { achievements: Response }) => {
const trophies = achievements.items[0].achievements
Expand All @@ -37,7 +39,7 @@ export function useTrophies({
acc[achievement.key] = achievement;
return acc;
}, {});
setTrophies((previous) => ({ ...trophies, ...previous }));
setRawTrophies({ ...trophies });
},
},
);
Expand All @@ -52,5 +54,23 @@ export function useTrophies({
}
}, [namespace, project, fetchAchievements]);

return { trophies: Object.values(trophies), isFetching };
useEffect(() => {
if (isFetching) return;
// Merge trophies
const trophies: { [id: string]: Trophy } = {};
Object.values(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;
}
});
setTrophies(trophies);
}, [rawTrophies, isFetching, setTrophies]);

return { trophies };
}

0 comments on commit 5905332

Please sign in to comment.