Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: calculate estimated PNK #60

Merged
merged 6 commits into from
May 20, 2024
Merged
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
3 changes: 2 additions & 1 deletion src/app/(main)/leaderboard/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import RewardsIcon from "@/assets/rewards.svg";
import { darkTheme } from "@kleros/ui-components-library";
import { Database } from "@/types/supabase";
import { isGameConcluded } from "@/lib/game.config";
import { formatNumber } from "@/lib/utils";

export const TableContainer = styled.div`
display: grid;
Expand Down Expand Up @@ -68,7 +69,7 @@ const Table: React.FC = () => {
<TableCellWrapper isGameConcluded={gameConcluded}>
<TableCell>{item.connections}</TableCell>
{gameConcluded && <TableCell>{item.points}</TableCell>}
<TableCell>~{item.token} PNK</TableCell>
<TableCell>~{formatNumber(item.token)} PNK</TableCell>
</TableCellWrapper>
</React.Fragment>
))}
Expand Down
5 changes: 3 additions & 2 deletions src/app/(main)/leaderboard/UserPoints.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useQuery } from "@tanstack/react-query";
import { darkTheme } from "@kleros/ui-components-library";
import { Database } from "@/types/supabase";
import { isGameConcluded } from "@/lib/game.config";
import { formatNumber } from "@/lib/utils";
import { TableContainer, TableCellWrapper, TableCell } from "./Table";

const Container = styled.div`
Expand All @@ -30,7 +31,7 @@ const UserPoints: React.FC = () => {

if (isPending) return <div>please wait...</div>;

if (error) return <div>{error.message} an error occured</div>;
if (error) return <div>something went wrong...</div>;

return (
<Container>
Expand All @@ -46,7 +47,7 @@ const UserPoints: React.FC = () => {
<TableCellWrapper isGameConcluded={gameConcluded}>
<TableCell>{data.connections}</TableCell>
{gameConcluded && <TableCell>{data.points}</TableCell>}
<TableCell>~{data.token}PNK</TableCell>
<TableCell>~{formatNumber(data.token)} PNK</TableCell>
</TableCellWrapper>
</TableContainer>
</Container>
Expand Down
14 changes: 13 additions & 1 deletion src/app/(main)/leaderboard/claim/ClaimReward.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import LightLinkButton from "@/components/LightLinkButton";
import LabeledInput from "@/components/LabeledInput";
import useClaimReward from "@/hooks/useClaimReward";
import { isGameConcluded } from "@/lib/game.config";
import { formatNumber } from "@/lib/utils";
import { TableContainer, TableCellWrapper, TableCell } from "../Table";
import UserPoints from "../UserPoints";

Expand Down Expand Up @@ -39,6 +40,11 @@ const Heading = styled.h2`
font-weight: 400;
`;

const Note = styled.p`
font-size: 12px;
color: ${({ theme }) => theme.klerosUIComponentsSecondaryBlue};
`;

const StyledLinkButton = styled(LightLinkButton)`
width: 100%;
fill: black;
Expand Down Expand Up @@ -94,7 +100,9 @@ const ClaimReward: React.FC<ClaimRewardProps> = ({ setClaimed }) => {
</TableCellWrapper>
</TableContainer>
<UserPoints />
<ClaimAmount>{!isPending && <>Amount: {data?.token} PNK</>}</ClaimAmount>
<ClaimAmount>
{!isPending && <>Amount: ~{formatNumber(data?.token!)} PNK</>}
</ClaimAmount>
<ClaimSection>
Type the address you want to receive the rewards
<LabeledInput
Expand All @@ -109,6 +117,10 @@ const ClaimReward: React.FC<ClaimRewardProps> = ({ setClaimed }) => {
text={isLoading ? "Submitting..." : "Submit Address"}
/>
</ClaimSection>
<Note>
The PNK shown is just an estimate, the true value will depend on your
performance
</Note>
</Container>
);
};
Expand Down
12 changes: 11 additions & 1 deletion src/app/(main)/leaderboard/claim/RewardClaimed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useQuery } from "@tanstack/react-query";
import { darkTheme } from "@kleros/ui-components-library";
import LightLinkButton from "@/components/LightLinkButton";
import { TOKEN_DISTRIBUTION_TIMESTAMP } from "@/lib/game.config";
import { formatNumber } from "@/lib/utils";

const Container = styled.div`
display: flex;
Expand Down Expand Up @@ -32,6 +33,11 @@ const StyledMessage = styled(StyledText)`
padding: 10px;
`;

const Note = styled.p`
font-size: 12px;
color: ${({ theme }) => theme.klerosUIComponentsSecondaryBlue};
`;

const StyledLinkButton = styled(LightLinkButton)`
width: 100%;
`;
Expand All @@ -52,7 +58,7 @@ const RewardClaimed: React.FC = () => {
<Container>
<StyledText>Well Done!</StyledText>
<Heading>Address Submitted!</Heading>
<StyledH2>{!isPending && <>{data?.token} PNK</>}</StyledH2>
<StyledH2>{!isPending && <>{formatNumber(data?.token!)} PNK</>}</StyledH2>
<StyledMessage>
Submitted! You will receive them before{" "}
{new Date(TOKEN_DISTRIBUTION_TIMESTAMP * 1000).toLocaleDateString(
Expand All @@ -66,6 +72,10 @@ const RewardClaimed: React.FC = () => {
url="/leaderboard"
text="Return"
/>
<Note>
The PNK shown is just an estimate, the true value will depend on your
performance
</Note>
</Container>
);
};
Expand Down
9 changes: 9 additions & 0 deletions src/app/(main)/leaderboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const Heading = styled.h2`
font-weight: 400;
`;

const Note = styled.p`
font-size: 12px;
color: ${({ theme }) => theme.klerosUIComponentsSecondaryBlue};
`;

const StyledLinkButton = styled(LightLinkButton)`
width: 100%;
margin-top: 28px;
Expand All @@ -32,6 +37,10 @@ const Leaderboard: React.FC = () => {
<Heading>Leaderboard</Heading>
<Table />
<UserPoints />
<Note>
The PNK shown is just an estimate, the true value will depend on your
performance
</Note>
<StyledLinkButton
url={gameEnded ? "/leaderboard/claim" : "/"}
Icon={gameEnded && RewardsIcon}
Expand Down
32 changes: 27 additions & 5 deletions src/app/api/leaderboard/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,34 @@
import { NextResponse } from "next/server";
import { getLeaderboard } from "@/lib/supabase/queries";
import {
getLeaderboard,
getTotalConnectionCount,
} from "@/lib/supabase/queries";
import { isGameConcluded, TOTAL_PNK } from "@/lib/game.config";

export const dynamic = "force-dynamic";

export const GET = async () => {
const { data, error } = await getLeaderboard();
if (error) {
return new NextResponse("No data found", { status: 404 });
const { data: leaderboard, error: leaderboardError } = await getLeaderboard();
const { data: totalConnectionCount, error: totalConnectionCountError } =
await getTotalConnectionCount();

if (leaderboardError || totalConnectionCountError) {
const errorMessage = leaderboardError || totalConnectionCountError;
return new NextResponse(String(errorMessage), { status: 500 });
}

if (leaderboard && totalConnectionCount) {
if (!isGameConcluded()) {
// Total_PNK * Player's_Connections * 0.7 / Total_Connections
leaderboard.forEach((item) => {
item.token = Math.floor(
(TOTAL_PNK * item.connections * 0.7) /
totalConnectionCount[0].total_count
);
});
}
return new NextResponse(JSON.stringify(leaderboard));
}
return new NextResponse(JSON.stringify(data));

return new NextResponse("No Data Found", { status: 404 });
};
23 changes: 17 additions & 6 deletions src/app/api/userstats/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type NextRequest, NextResponse } from "next/server";
import { getUserId, TOKEN_COOKIE, NotAuthenticatedResponse } from "@/lib/auth";
import { getUserStats } from "@/lib/supabase/queries";
import { getUserStats, getTotalConnectionCount } from "@/lib/supabase/queries";
import { isGameConcluded, TOTAL_PNK } from "@/lib/game.config";

export const GET = async (request: NextRequest) => {
const token = request.cookies.get(TOKEN_COOKIE)?.value;
Expand All @@ -10,14 +11,24 @@ export const GET = async (request: NextRequest) => {
return NotAuthenticatedResponse;
}

const { data, error } = await getUserStats(userId);
const { data: userStats, error: userStatsError } = await getUserStats(userId);
const { data: totalConnectionCount, error: totalConnectionCountError } =
await getTotalConnectionCount();

if (error) {
return new NextResponse(String(error), { status: 404 });
if (userStatsError || totalConnectionCountError) {
const errorMessage = userStatsError || totalConnectionCountError;
return new NextResponse(String(errorMessage), { status: 500 });
}

if (data) {
return new NextResponse(JSON.stringify(data[0]));
if (userStats && totalConnectionCount) {
if (!isGameConcluded()) {
// Total_PNK * Player's_Connections * 0.7 / Total_Connections
userStats[0].token = Math.floor(
(TOTAL_PNK * userStats[0].connections * 0.7) /
totalConnectionCount[0].total_count
);
}
return new NextResponse(JSON.stringify(userStats[0]));
}

return new NextResponse("Data not found", { status: 404 });
Expand Down
2 changes: 2 additions & 0 deletions src/lib/game.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const TOKEN_DISTRIBUTION_TIMESTAMP = 1722211200; // Timestamp in seconds
export const QR_CODE_EXPIRY =2 * 60 * 1000; // (2 minutes) Expiry time in seconds for QR code
export const QUESTION_TIMEOUT_WINDOW = 3 * 60 * 1000; // (3 minutes) Player cannot answer after question gets timeout

export const TOTAL_PNK = 1000000; // Total PNK to be distributed

export const isGameEnded = (): boolean => {
const currentTimestamp = Math.floor(Date.now() / 1000);
return currentTimestamp > GAME_END_TIMESTAMP;
Expand Down
8 changes: 8 additions & 0 deletions src/lib/supabase/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ export const updateConnectionCount = async (user_id: UUID) => {
return { data, error };
};

export const getTotalConnectionCount = async () => {
const { data, error } = await supabase
.rpc("get_total_connection_count")
.select();

return { data, error };
};

export const getLeaderboard = async () => {
const { data, error } = await supabase
.from("leaderboard")
Expand Down
10 changes: 10 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
export const isUndefined = (maybeObject: any): maybeObject is undefined | null =>
typeof maybeObject === "undefined" || maybeObject === null;

export const formatNumber = (number: number): string => {
if (number >= 1000000) {
return Math.floor(number / 1000000) + "m";
} else if (number >= 1000) {
return Math.floor(number / 1000) + "k";
} else {
return number.toString();
}
};
6 changes: 6 additions & 0 deletions src/types/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ export type Database = {
[_ in never]: never
}
Functions: {
get_total_connection_count: {
Args: Record<PropertyKey, never>
Returns: {
total_count: number
}[]
}
get_user_stats: {
Args: {
user_id_params: string
Expand Down
12 changes: 12 additions & 0 deletions supabase/migrations/20240520052025_get_total_connection_count.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
set check_function_bodies = off;

CREATE OR REPLACE FUNCTION public.get_total_connection_count()
RETURNS TABLE(total_count integer)
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT SUM(connections) INTO total_count FROM leaderboard;
RETURN NEXT;
END;
$function$
;
Loading