Skip to content
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6,762 changes: 1,677 additions & 5,085 deletions package-lock.json

Large diffs are not rendered by default.

36 changes: 35 additions & 1 deletion src/atlas_frontend/src/canisters/atlasMain/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ export const getAtlasUser = async ({
...userData,
in_hub: userData.in_hub.pop() ?? null,
owned_spaces: Array.from(userData.owned_spaces),
belonging_to_spaces: Array.from(userData.owned_spaces),
belonging_to_spaces: Array.from(userData.belonging_to_spaces),
deci_xp_points: userData.deci_xp_points,
referral_rewards: Array.from(userData.referral_rewards),
})
);
} finally {
Expand Down Expand Up @@ -259,3 +261,35 @@ export const getSpaceUsersCount = async ({
});
dispatch(setSpaceUsersCount({spaceId: spaceId.toString(), count}));
};

interface RegisterReferralRewardArgs {
authAtlasMain: ActorSubclass<_SERVICE_MAIN>;
user: Principal;
spacePrincipal: Principal;
taskId: bigint;
invitee: Principal;
amount: bigint;
}

export const registerReferralReward = async ({
authAtlasMain,
user,
spacePrincipal,
taskId,
invitee,
amount,
}: RegisterReferralRewardArgs) => {
const call = authAtlasMain.register_referral_reward(
user,
spacePrincipal,
taskId,
invitee,
amount,
);

await unwrapCall<null>({
call,
errMsg: "Failed to register referral reward",
});
};

19 changes: 19 additions & 0 deletions src/atlas_frontend/src/canisters/atlasSpace/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,3 +436,22 @@ export const deleteClosedTask = async ({
errMsg: "Failed to delete closed task",
});
};

interface RegisterReferralArgs {
authAtlasSpace: ActorSubclass<_SERVICE>;
taskId: bigint;
inviter: Principal;
}

export const registerReferral = async ({
authAtlasSpace,
taskId,
inviter,
}: RegisterReferralArgs) => {
const call = authAtlasSpace.register_task_referral(taskId, inviter);

await unwrapCall<null>({
call,
errMsg: "Failed to join task with referral",
});
};
77 changes: 52 additions & 25 deletions src/atlas_frontend/src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
import { getAtlasConfig, getAtlasUser } from "../../canisters/atlasMain/api.ts";
import {
ADMIN_PATH,
REFERRALS_PATH,
getCreateTaskPath,
SPACES_PATH,
WALLET_PATH,
Expand All @@ -39,6 +40,7 @@ import { RiAddLine, RiWalletFill, RiLogoutBoxRLine } from 'react-icons/ri';
import { getCkUsdcBalance } from "../../hooks/balances.ts";
import { FaGear } from 'react-icons/fa6';
import { useSpaceNavigation } from "../../hooks/useSpaceNavigation.ts";
import { championLevels, deciXPtoXP, getChampionLevel, getNextLevelInfo } from "../../utils/xp.ts";

const ConnectButton = (props: ConnectWalletButtonProps) => (
<Button
Expand Down Expand Up @@ -111,6 +113,14 @@ const DropdownMenuComponent = ({
const parsedUserCkUsdc =
userCkUsdc !== null ? formatUnits(userCkUsdc, DECIMALS) : null;

const xpNumber = deciXPtoXP(userInfo?.deci_xp_points ?? 0n);
const levelKey = getChampionLevel(xpNumber);
const levelInfo = championLevels[levelKey];
const nextLevelInfo = getNextLevelInfo(xpNumber);
const progress = nextLevelInfo.xpNeeded !== null
? (xpNumber - levelInfo.minXp) / (nextLevelInfo.xpNeeded + (xpNumber - levelInfo.minXp))
: 1;

return (
<>
<Menu>
Expand Down Expand Up @@ -148,17 +158,28 @@ const DropdownMenuComponent = ({
</div>
</MenuItem>
</div>
{/* <div className="w-full h-[1px] bg-light2/20 my-2" />
<div className="flex flex-col px-3 pt-2">
<p className="text-primary font-montserrat font-medium">
Rank
</p>
<p className="text-light font-montserrat font-medium">
{rank}
</p>
</div>
<ExperienceProgressBar exp={324} expToLvlUp={500} /> */}
<div className="w-full h-[1px] bg-light2/20 my-2" />
{!userInfo?.isAdmin() && xpNumber !== null && (
<MenuItem>
<div className="flex flex-col gap-2 mt-2 w-full">
<div className="bg-[#1E0F33] px-4 py-2 rounded-md text-center font-montserrat font-medium">
{xpNumber} XP • {levelInfo.title}
</div>
<div className="w-full bg-gray-300 h-3 overflow-hidden">
<div
className="bg-[#9173FF] h-3 transition-all"
style={{ width: `${progress * 100}%` }}
/>
</div>
{nextLevelInfo.xpNeeded !== null && (
<div className="text-xs text-center">
{nextLevelInfo.xpNeeded} XP to {nextLevelInfo.nextTitle}
</div>
)}
</div>
</MenuItem>
)}

{parsedUserCkUsdc !== null && (
<MenuItem>
<div
Expand Down Expand Up @@ -301,7 +322,7 @@ const Navbar = () => {
</a>
<div className="flex items-center justify-center gap-2 sm:gap-3 md:gap-4">
{user && (
<div className="flex">
<div className="flex gap-2 sm:gap-3 md:gap-4">
<Button
variant={location?.pathname !== "/space" ? "publish" : "primary"}
onClick={() => navigate(SPACES_PATH)}
Expand All @@ -312,26 +333,32 @@ const Navbar = () => {
{/* <Button light={location?.pathname !== "/space/leaderboard"}>
Leaderboard
</Button> */}
{/* <Button light={location?.pathname !== "/space/referrals"}>
Referrals
</Button> */}
</div>
)}
<ConnectWallet
connectButtonComponent={ConnectButton}
dropdownMenuComponent={DropdownMenuComponent}
/>
{!userInfo?.isAdmin() && (
<Button
variant={location?.pathname !== "/referrals" ? "publish" : "primary"}
onClick={() => navigate(REFERRALS_PATH)}
className="font-medium text-[12px] md:text-base px-4 md:px-6 "
>
Referrals
</Button>
)}
</div>
)}
<ConnectWallet
connectButtonComponent={ConnectButton}
dropdownMenuComponent={DropdownMenuComponent}
/>
{!user && (
<Button
variant="saveDraft"
className="text-[14px] px-3 md:text-[18px] md:w-[110px] md:h-[33px] md:px-4 md:2my-2 sm:my-0"
onClick={() => navigate(SPACES_PATH)}>
Discover
variant="saveDraft"
className="text-[14px] px-3 md:text-[18px] md:w-[110px] md:h-[33px] md:px-4 md:2my-2 sm:my-0"
onClick={() => navigate(SPACES_PATH)}>
Discover
</Button>
)}
</div>
</div>
</div>
</div>
);
};

Expand Down
112 changes: 112 additions & 0 deletions src/atlas_frontend/src/components/Referrals/ReferralsSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from "react";
import { useSelector } from "react-redux";
import { deserialize } from "../../store/store";
import { selectUserBlockchainData, type StorableUser } from "../../store/slices/userSlice";
import { deciXPtoXP, getChampionLevel, getChampionTitle, getMaxReferralsForLevel } from "../../utils/xp";
import { mediumPrincipal } from "../../utils/icp";

const ReferralSection = () => {
const userData = deserialize<StorableUser>(
useSelector(selectUserBlockchainData)
);

if (!userData) return null;

const totalReferralPoints = userData.referral_rewards.reduce(
(acc, r) => acc + deciXPtoXP(r.points),
0
);

const rewardsByUser: Record<string, { tasks: number[]; points: number }> = {};
userData.referral_rewards.forEach(r => {
const user = r.invitee.toText();
if (!rewardsByUser[user]) {
rewardsByUser[user] = { tasks: [], points: 0 };
}
rewardsByUser[user].tasks.push(Number(r.task_id));
rewardsByUser[user].points += deciXPtoXP(r.points);
});

const rewardsArray = Object.entries(rewardsByUser).map(([user, data]) => ({
user,
tasks: data.tasks.join(", "),
points: data.points,
}));

const championLevel = getChampionLevel(deciXPtoXP(userData.deci_xp_points));
const maxReferrals = getMaxReferralsForLevel(championLevel);
const championTitle = getChampionTitle(championLevel);

const totalReferralsUsed = userData.referral_rewards.length;
const remainingReferrals = maxReferrals - totalReferralsUsed;

return (
<div className="relative w-full rounded-xl overflow-hidden font-montserrat mt-8">
<div className="absolute inset-0 bg-[#1E0F33] z-0" />

<div className="relative px-6 py-6 flex flex-col gap-6 text-white">
<div className="flex flex-col md:flex-row gap-4 md:gap-8 justify-between">
<div className="w-full md:w-1/3 min-h-[160px] bg-white/10 backdrop-blur-sm rounded-2xl text-left p-6 flex flex-col justify-center">
<div className="text-2xl font-bold">Your Referral Earnings</div>
<div className="mt-6 flex items-baseline gap-2">
<span className="text-xl font-semibold">{totalReferralPoints}</span>
<span className="text-sm text-gray-300">points</span>
</div>
</div>

<div className="w-full md:w-2/3 min-h-[160px] bg-[url(/reward-bg-img.png)] bg-cover bg-center [mix-blend-mode:luminosity] backdrop-blur-sm rounded-2xl text-center flex flex-col justify-center">
<div className="text-xl font-semibold">Max referral rewards uses for rank ({championTitle}): </div>
<div className="text-3xl text-[#00FFAA] font-bold mt-2">{maxReferrals}</div>
<div className="mt-2 text-base text-gray-300">
Remaining referral rewards uses:{" "}
<span className="text-white font-semibold">{remainingReferrals}</span>
</div>
</div>
</div>

<div className="bg-[#9173FF]/20 backdrop-blur-sm p-6 rounded-2xl flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="flex flex-col sm:flex-row items-center justify-between w-full gap-4">
<div className="text-xl font-semibold">Current single referral reward</div>
<div className="flex-1 flex justify-center">
<div className="text-white border-b border-white px-4 pb-1">
{"+2 CkUsdc"}
</div>
</div>
</div>
</div>

<div className="flex flex-col md:flex-row gap-4">
<div className="md:w-1/3 flex flex-col">
<div className="bg-white/10 backdrop-blur-sm p-3 h-1/3 min-h-[80px] rounded-2xl text-3xl font-semibold text-center flex items-end justify-center">
Your Referrals
</div>
</div>

<div className="md:w-2/3 backdrop-blur-sm p-6 rounded-2xl"
style={{
background: "linear-gradient(180deg, rgba(217, 217, 217, 0.05) 0%, rgba(145, 115, 255, 0.01) 100%)"
}}
>
<div className="grid grid-cols-3 gap-4 text-[#9173FF] font-semibold border-b border-[#9173FF] pb-2">
<div>User</div>
<div>Task ID</div>
<div>Points</div>
</div>

<div className="mt-2">
{rewardsArray.map((r, index) => (
<div key={index} className="grid grid-cols-3 gap-4 text-white py-1">
<div>{mediumPrincipal(r.user)}</div>
<div>{r.tasks}</div>
<div>{r.points}</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
};

export default ReferralSection;
25 changes: 25 additions & 0 deletions src/atlas_frontend/src/components/Referrals/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from "react";
import { useAuth } from "@nfid/identitykit/react";
import { useNavigate } from "react-router-dom";
import { authGuard } from "../../hooks/guard";
import ReferralSection from "./ReferralsSection";

const Refferals = () => {
const navigate = useNavigate();
const { user } = useAuth();

authGuard({
navigate,
user,
});

return (
<div className="container mx-auto my-4">
<div className="w-full px-3">
<ReferralSection />
</div>
</div>
);
};

export default Refferals;
Loading