From d18ed862244ec90de406c90e494eae019a6c3b8f Mon Sep 17 00:00:00 2001 From: Innei Date: Wed, 23 Oct 2024 13:34:32 +0800 Subject: [PATCH] feat: more achievemnt (#964) * feat: achievemnt Signed-off-by: Innei * fix: comment Signed-off-by: Innei * feat: audit Signed-off-by: Innei * server config Signed-off-by: Innei * config Signed-off-by: Innei * chore: auto-fix linting and formatting issues * refactor: extract mint button Signed-off-by: Innei * feat: alpha tester Signed-off-by: Innei --------- Signed-off-by: Innei Signed-off-by: Innei Co-authored-by: Innei --- .../achievement/AchievementModalContent.tsx | 398 +++++++++++++----- locales/app/en.json | 14 + locales/errors/en.json | 2 + packages/shared/src/hono.ts | 28 +- 4 files changed, 326 insertions(+), 116 deletions(-) diff --git a/apps/renderer/src/modules/achievement/AchievementModalContent.tsx b/apps/renderer/src/modules/achievement/AchievementModalContent.tsx index 688bcec05..5b06768cc 100644 --- a/apps/renderer/src/modules/achievement/AchievementModalContent.tsx +++ b/apps/renderer/src/modules/achievement/AchievementModalContent.tsx @@ -1,15 +1,20 @@ import { DotLottieReact } from "@lottiefiles/dotlottie-react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import type { SingletonRefObject } from "foxact/use-singleton" import { useSingleton } from "foxact/use-singleton" +import type { PrimitiveAtom } from "jotai" import { atom, useStore } from "jotai" import { nanoid } from "nanoid" import type { FC } from "react" -import { useEffect } from "react" -import { Trans } from "react-i18next" +import { useEffect, useId, useMemo, useRef } from "react" +import { Trans, useTranslation } from "react-i18next" +import { useServerConfigs } from "~/atoms/server-configs" import { Button } from "~/components/ui/button" import { styledButtonVariant } from "~/components/ui/button/variants" +import { Input } from "~/components/ui/input" import { LoadingCircle, LoadingWithIcon } from "~/components/ui/loading" +import { useCurrentModal, useModalStack } from "~/components/ui/modal" import { useI18n } from "~/hooks/common" import { apiClient } from "~/lib/api-fetch" import { Chain } from "~/lib/chain" @@ -19,6 +24,14 @@ import achievementAnimationUri from "~/lottie/achievement.lottie?url" const absoluteachievementAnimationUri = new URL(achievementAnimationUri, import.meta.url).href enum AchievementsActionIdMap { FIRST_CLAIM_FEED = 0, + FIRST_CREATE_LIST = 1, + LIST_SUBSCRIBE_50 = 2, + LIST_SUBSCRIBE_100 = 3, + LIST_SUBSCRIBE_500 = 4, + PRODUCT_HUNT_VOTE = 5, + // TODO + // FOLLOW_SPECIAL_FEED = 6, + ALPHA_TESTER = 7, } const achievementActionIdCopyMap: Record< @@ -29,20 +42,46 @@ const achievementActionIdCopyMap: Record< title: "achievement.first_claim_feed", description: "achievement.first_claim_feed_description", }, + [AchievementsActionIdMap.FIRST_CREATE_LIST]: { + title: "achievement.first_create_list", + description: "achievement.first_create_list_description", + }, + [AchievementsActionIdMap.LIST_SUBSCRIBE_50]: { + title: "achievement.list_subscribe_50", + description: "achievement.list_subscribe_50_description", + }, + [AchievementsActionIdMap.LIST_SUBSCRIBE_100]: { + title: "achievement.list_subscribe_100", + description: "achievement.list_subscribe_100_description", + }, + [AchievementsActionIdMap.LIST_SUBSCRIBE_500]: { + title: "achievement.list_subscribe_500", + description: "achievement.list_subscribe_500_description", + }, + [AchievementsActionIdMap.PRODUCT_HUNT_VOTE]: { + title: "achievement.product_hunt_vote", + description: "achievement.product_hunt_vote_description", + }, + // [AchievementsActionIdMap.FOLLOW_SPECIAL_FEED]: { + // title: "achievement.follow_special_feed", + // description: "achievement.follow_special_feed_description", + // }, + [AchievementsActionIdMap.ALPHA_TESTER]: { + title: "achievement.alpha_tester", + description: "achievement.alpha_tester_description", + }, } - +const _achievementType = apiClient.achievement.$get({ + query: { + type: "all", + }, +}) +type Achievement = Awaited["data"] export const AchievementModalContent: FC = () => { const jotaiStore = useStore() - const queryClient = useQueryClient() const defaultAchievements = useSingleton(buildDefaultAchievements) - const _achievementType = apiClient.achievement.$get({ - query: { - type: "all", - }, - }) - type Achievement = Awaited["data"] const { data: achievements, isLoading, @@ -66,6 +105,7 @@ export const AchievementModalContent: FC = () => { }) useEffect(() => { + if (isLoading) return if (!achievements) return let shouldPolling = false const pollingChain = new Chain() @@ -85,32 +125,20 @@ export const AchievementModalContent: FC = () => { return () => { pollingChain.abort() } - }, [achievements, refetch]) + }, [achievements, isLoading, refetch]) - const { mutateAsync: mintAchievement, isPending: isMinting } = useMutation({ - mutationFn: async (actionId: number) => { - return apiClient.achievement.$put({ - json: { - actionId, - }, - }) - }, - }) const achievementsDataAtom = useSingleton(() => atom()) - const { mutateAsync: checkAchievement, isPending: checkPending } = useMutation({ - mutationFn: async (actionId: number) => { - return apiClient.achievement.check.$post({ - json: { - actionId, - }, - }) - }, - onSuccess: () => { - refetch() - }, - }) + const t = useI18n() + const sortedAchievements = useMemo(() => { + return achievements?.sort((a, b) => { + if (a.type === "received") return 1 + if (b.type === "received") return -1 + return 0 + }) + }, [achievements]) + return (
{ } size="large" />
) : ( - achievements?.map((achievement) => { + sortedAchievements?.map((achievement) => { const copy = achievementActionIdCopyMap[achievement.actionId] if (!copy) return null @@ -156,7 +184,7 @@ export const AchievementModalContent: FC = () => { {achievement.power && ( {achievement.power} - + )} @@ -164,7 +192,6 @@ export const AchievementModalContent: FC = () => { {t(copy.description)} - {achievement.type === "checking" && (
{ {t("words.mint")}
)} - {achievement.type === "incomplete" && ( - + )} - {achievement.type === "received" && (
{ {t("achievement.all_done")}
)} - {achievement.type === "completed" && ( - + Validating... + + )} + {achievement.type === "completed" && ( + )} ) @@ -271,13 +246,216 @@ export const AchievementModalContent: FC = () => { } const buildDefaultAchievements = () => { - return [ - { + return Object.keys(achievementActionIdCopyMap).map((key) => { + return { id: nanoid(), - actionId: AchievementsActionIdMap.FIRST_CLAIM_FEED, + actionId: Number(key), type: "checking", - progress: 0, - progressMax: 0, - }, - ] + } + }) +} + +const MintButton: FC<{ + achievementsDataAtom: SingletonRefObject> + achievement: Achievement[number] +}> = ({ achievementsDataAtom, achievement }) => { + const { mutateAsync: mintAchievement, isPending: isMinting } = useMutation({ + mutationFn: async (actionId: number) => { + return apiClient.achievement.$put({ + json: { + actionId, + }, + }) + }, + }) + + const jotaiStore = useStore() + const queryClient = useQueryClient() + const { t } = useTranslation() + return ( + + ) +} + +const IncompleteButton: FC<{ + achievement: Achievement[number] + refetch: () => void +}> = ({ achievement, refetch }) => { + const { mutateAsync: checkAchievement, isPending: checkPending } = useMutation({ + mutationFn: async (actionId: number) => { + return apiClient.achievement.check.$post({ + json: { + actionId, + }, + }) + }, + onSuccess: () => { + refetch() + }, + }) + + const { present } = useModalStack() + const Content = useMemo(() => { + switch (achievement.actionId) { + case AchievementsActionIdMap.PRODUCT_HUNT_VOTE: { + return ( + + ) + } + default: { + return ( + <> +
+ +
+
+ + + {achievement.progress} / {achievement.progressMax} + + + + + +
+ + ) + } + } + }, [ + achievement.actionId, + achievement.progress, + achievement.progressMax, + checkPending, + present, + refetch, + ]) + + return ( + + ) +} +const VoteValidateModalContent: FC<{ refetch: () => void }> = ({ refetch }) => { + const ref = useRef(null) + const { dismiss } = useCurrentModal() + const { present } = useModalStack() + const { mutateAsync: audit, isPending } = useMutation({ + mutationFn: (username: string) => { + return apiClient.achievement.audit.$post({ + json: { + actionId: AchievementsActionIdMap.PRODUCT_HUNT_VOTE, + payload: { + username, + }, + }, + }) + }, + onSuccess: () => { + dismiss() + + refetch() + present({ + title: "Thank you!", + content: () =>
Thank you for your vote. Please wait for our verification.
, + clickOutsideToDismiss: true, + }) + }, + }) + const { PRODUCT_HUNT_VOTE_URL } = useServerConfigs() || {} + const id = useId() + + return ( +
{ + e.preventDefault() + if (!ref.current?.value) return + audit(ref.current.value) + }} + > + +
+ +
+ +
+ +
+
+ ) } diff --git a/locales/app/en.json b/locales/app/en.json index a4603d4d4..f8ba62f13 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -1,8 +1,22 @@ { "achievement.all_done": "All done!", + "achievement.alpha_tester": "Alpha Tester", + "achievement.alpha_tester_description": "You are an alpha tester of Follow", "achievement.first_claim_feed": "Feed Owner", "achievement.first_claim_feed_description": "You own your feed on Follow", + "achievement.first_create_list": "List Creator", + "achievement.first_create_list_description": "You created your first list on Follow", + "achievement.follow_special_feed": "Special Feed Follower", + "achievement.follow_special_feed_description": "You followed the special feed on Follow", + "achievement.list_subscribe_100": "100 List Subscriber", + "achievement.list_subscribe_100_description": "100 subscribers subscribed to the list you created", + "achievement.list_subscribe_50": "50 List Subscriber", + "achievement.list_subscribe_500": "500 List Subscriber", + "achievement.list_subscribe_500_description": "500 subscribers subscribed to the list you created", + "achievement.list_subscribe_50_description": "50 subscribers subscribed to the list you created", "achievement.mint_more_power": "Be a hardcore player and earn more ", + "achievement.product_hunt_vote": "Product Hunt Voter", + "achievement.product_hunt_vote_description": "You voted for Hunk in Product Hunt", "activation.activate": "Activate", "activation.description": "During the public beta phase, current feature is limited for inactive users.", "activation.title": "Invitation Code", diff --git a/locales/errors/en.json b/locales/errors/en.json index be763643f..d816cc54b 100644 --- a/locales/errors/en.json +++ b/locales/errors/en.json @@ -39,6 +39,8 @@ "8003": "Feed already added to list", "9000": "Achievement not completed", "9001": "Achievement already received", + "9002": "Achievement is under audit", + "9003": "Achievement audit not needed", "10000": "Inbox not found", "10001": "Inbox already exists", "10002": "Inbox limit exceeded", diff --git a/packages/shared/src/hono.ts b/packages/shared/src/hono.ts index 602be3a03..ca738a23e 100644 --- a/packages/shared/src/hono.ts +++ b/packages/shared/src/hono.ts @@ -52,14 +52,14 @@ declare const achievements: drizzle_orm_pg_core.PgTableWithColumns<{ tableName: "achievements"; dataType: "string"; columnType: "PgText"; - data: "received" | "checking" | "completed" | "incomplete"; + data: "received" | "checking" | "completed" | "incomplete" | "audit"; driverParam: string; notNull: true; hasDefault: false; isPrimaryKey: false; isAutoincrement: false; hasRuntimeDefault: false; - enumValues: ["checking", "completed", "incomplete", "received"]; + enumValues: ["checking", "completed", "incomplete", "audit", "received"]; baseColumn: never; generated: undefined; }, {}, {}>; @@ -165,7 +165,7 @@ declare const achievements: drizzle_orm_pg_core.PgTableWithColumns<{ declare const achievementsOpenAPISchema: zod.ZodObject<{ id: zod.ZodString; userId: zod.ZodString; - type: zod.ZodEnum<["checking", "completed", "incomplete", "received"]>; + type: zod.ZodEnum<["checking", "completed", "incomplete", "audit", "received"]>; actionId: zod.ZodNumber; progress: zod.ZodNumber; progressMax: zod.ZodNumber; @@ -173,7 +173,7 @@ declare const achievementsOpenAPISchema: zod.ZodObject<{ doneAt: zod.ZodNullable; tx: zod.ZodNullable; }, zod.UnknownKeysParam, zod.ZodTypeAny, { - type: "received" | "checking" | "completed" | "incomplete"; + type: "received" | "checking" | "completed" | "incomplete" | "audit"; id: string; userId: string; actionId: number; @@ -183,7 +183,7 @@ declare const achievementsOpenAPISchema: zod.ZodObject<{ doneAt: string | null; tx: string | null; }, { - type: "received" | "checking" | "completed" | "incomplete"; + type: "received" | "checking" | "completed" | "incomplete" | "audit"; id: string; userId: string; actionId: number; @@ -4487,6 +4487,7 @@ declare const _routes: hono_hono_base.HonoBase; type AppType = typeof _routes;