Skip to content

Commit

Permalink
feat: achievement badge
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <[email protected]>
  • Loading branch information
Innei committed Nov 16, 2024
1 parent a15f23a commit aceb4c9
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 9 deletions.
2 changes: 1 addition & 1 deletion apps/renderer/src/components/ui/media/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export const VideoPlayer = forwardRef<VideoPlayerRef, VideoPlayerProps>(
[state, controls, src, variant],
)}
>
{variant === "preview" && <FloatMutedButton />}
{variant === "preview" && state.hasAudio && <FloatMutedButton />}
{isPlayer && <ControlBar />}
</VideoPlayerContext.Provider>
</Focusable>
Expand Down
30 changes: 30 additions & 0 deletions apps/renderer/src/components/ui/modal/stacked/custom-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,33 @@ export const DrawerModalLayout: FC<PropsWithChildren> = ({ children }) => {
</div>
)
}

export const ScaleModal: ModalTemplateType = (props) => {
const { dismiss } = useCurrentModal()

return (
<div className={"container center h-full"} onPointerDown={dismiss} onClick={stopPropagation}>
<m.div
onPointerDown={stopPropagation}
transition={{
type: "spring",
mass: 0.4,
tension: 100,
friction: 1,
}}
initial={{ transform: "scale(0)", opacity: 0 }}
animate={{ transform: "scale(1)", opacity: 1 }}
exit={{ transform: "scale(0.6)", opacity: 0 }}
className="relative"
>
{props.children}
</m.div>
</div>
)
}

ScaleModal.class = (className: string) => {
return (props: ComponentType) => (
<ScaleModal {...props} className={cn(props.className, className)} />
)
}
89 changes: 81 additions & 8 deletions apps/renderer/src/modules/achievement/AchievementModalContent.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Button } from "@follow/components/ui/button/index.js"
import { RiNftFill } from "@follow/components/icons/nft.jsx"
import { Button, MotionButtonBase } from "@follow/components/ui/button/index.js"
import { styledButtonVariant } from "@follow/components/ui/button/variants.js"
import { Input } from "@follow/components/ui/input/Input.js"
import { LoadingCircle, LoadingWithIcon } from "@follow/components/ui/loading/index.jsx"
import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js"
import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.js"
import { useOnce } from "@follow/hooks"
import type { ExtractBizResponse } from "@follow/models/types"
import { Chain } from "@follow/utils/chain"
import { cn } from "@follow/utils/utils"
Expand All @@ -17,6 +19,8 @@ import { useTranslation } from "react-i18next"

import { useServerConfigs } from "~/atoms/server-configs"
import { LazyDotLottie } from "~/components/common/LazyDotLottie"
import { VideoPlayer } from "~/components/ui/media/VideoPlayer"
import { ScaleModal } from "~/components/ui/modal/stacked/custom-modal"
import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useI18n } from "~/hooks/common"
import { apiClient } from "~/lib/api-fetch"
Expand All @@ -36,33 +40,39 @@ enum AchievementsActionIdMap {
FEED_BOOSTER = 8,
}

const achievementActionIdCopyMap: Record<
const achievementActionIdMetaMap: Record<
AchievementsActionIdMap,
{ title: I18nKeys; description: I18nKeys }
{ title: I18nKeys; description: I18nKeys; video?: string }
> = {
[AchievementsActionIdMap.FIRST_CLAIM_FEED]: {
title: "achievement.first_claim_feed",
description: "achievement.first_claim_feed_description",
video: "https://assets.follow.is/FeedOwnerBadge.webm",
},
[AchievementsActionIdMap.FIRST_CREATE_LIST]: {
title: "achievement.first_create_list",
description: "achievement.first_create_list_description",
video: "https://assets.follow.is/ListBadge.webm",
},
[AchievementsActionIdMap.LIST_SUBSCRIBE_50]: {
title: "achievement.list_subscribe_50",
description: "achievement.list_subscribe_50_description",
video: "https://assets.follow.is/50Subs.webm",
},
[AchievementsActionIdMap.LIST_SUBSCRIBE_100]: {
title: "achievement.list_subscribe_100",
description: "achievement.list_subscribe_100_description",
video: "https://assets.follow.is/100Subs.webm",
},
[AchievementsActionIdMap.LIST_SUBSCRIBE_500]: {
title: "achievement.list_subscribe_500",
description: "achievement.list_subscribe_500_description",
video: "https://assets.follow.is/500Subs.webm",
},
[AchievementsActionIdMap.PRODUCT_HUNT_VOTE]: {
title: "achievement.product_hunt_vote",
description: "achievement.product_hunt_vote_description",
video: "https://assets.follow.is/ProductHunt.webm",
},
// [AchievementsActionIdMap.FOLLOW_SPECIAL_FEED]: {
// title: "achievement.follow_special_feed",
Expand All @@ -71,17 +81,33 @@ const achievementActionIdCopyMap: Record<
[AchievementsActionIdMap.ALPHA_TESTER]: {
title: "achievement.alpha_tester",
description: "achievement.alpha_tester_description",
video: "https://assets.follow.is/AlphaBadge.webm",
},
[AchievementsActionIdMap.FEED_BOOSTER]: {
title: "achievement.feed_booster",
description: "achievement.feed_booster_description",
},
}

const prefetchVideos = () => {
Object.values(achievementActionIdMetaMap).forEach(({ video }) => {
if (video) {
const link = document.createElement("link")
link.rel = "prefetch"
link.href = video
document.head.append(link)
}
})
}

type Achievement = ExtractBizResponse<typeof apiClient.achievement.$get>["data"]
export const AchievementModalContent: FC = () => {
const jotaiStore = useStore()

useOnce(() => {
prefetchVideos()
})

const defaultAchievements = useState(buildDefaultAchievements)[0]

const {
Expand Down Expand Up @@ -140,6 +166,33 @@ export const AchievementModalContent: FC = () => {
return 0
})
}, [achievements])
const { present } = useModalStack()

const presentBadgeVideo = (type: keyof typeof achievementActionIdMetaMap) => {
const copy = achievementActionIdMetaMap[type]
if (!copy) return
const { video } = copy
if (!video) return
present({
title: "Congratulations!",
CustomModalComponent: ScaleModal,
content: ({ dismiss }) => (
<VideoPlayer
variant="preview"
onClick={dismiss}
aria-label="Close video"
src={video}
autoPlay
muted
height={500}
width={500}
loop
/>
),

overlay: true,
})
}

return (
<div className="relative flex w-full grow flex-col items-center">
Expand All @@ -166,13 +219,26 @@ export const AchievementModalContent: FC = () => {
</div>
) : (
sortedAchievements?.map((achievement) => {
const copy = achievementActionIdCopyMap[achievement.actionId]
const copy = achievementActionIdMetaMap[achievement.actionId]
if (!copy) return null

return (
<li key={achievement.id} className="flex items-center justify-between">
<div>
<div className="text-base font-bold">{t(copy.title)}</div>
<div className="flex items-center text-base font-bold">
{t(copy.title)}

{copy.video && (
<MotionButtonBase
className="p-1 duration-200 hover:text-accent"
onClick={() => {
presentBadgeVideo(achievement.actionId)
}}
>
<RiNftFill className="size-4" />
</MotionButtonBase>
)}
</div>
<div className="flex items-center text-sm text-muted-foreground">
{t(copy.description)}
</div>
Expand Down Expand Up @@ -217,6 +283,9 @@ export const AchievementModalContent: FC = () => {
)}
{achievement.type === "completed" && (
<MintButton
onMinted={() => {
presentBadgeVideo(achievement.actionId)
}}
achievementsDataAtom={achievementsDataAtom}
achievement={achievement}
/>
Expand All @@ -228,13 +297,15 @@ export const AchievementModalContent: FC = () => {
</ul>
</ScrollArea>

<p className="mt-4 pb-2 text-xs text-muted-foreground">{t("achievement.nft_coming_soon")}</p>
<p className="mt-4 pb-2 text-xs text-muted-foreground">
* {t("achievement.nft_coming_soon")}
</p>
</div>
)
}

const buildDefaultAchievements = () => {
return Object.keys(achievementActionIdCopyMap).map((key) => {
return Object.keys(achievementActionIdMetaMap).map((key) => {
return {
id: nanoid(),
actionId: Number(key),
Expand All @@ -246,7 +317,8 @@ const buildDefaultAchievements = () => {
const MintButton: FC<{
achievementsDataAtom: PrimitiveAtom<Achievement | undefined>
achievement: Achievement[number]
}> = ({ achievementsDataAtom, achievement }) => {
onMinted: () => void
}> = ({ achievementsDataAtom, achievement, onMinted }) => {
const { mutateAsync: mintAchievement, isPending: isMinting } = useMutation({
mutationFn: async (actionId: number) => {
return apiClient.achievement.$put({
Expand Down Expand Up @@ -287,6 +359,7 @@ const MintButton: FC<{
if (shouldInvalidate) {
queryClient.invalidateQueries({ queryKey: ["achievements"] })
}
onMinted()
}}
>
{t("words.mint")}
Expand Down
13 changes: 13 additions & 0 deletions packages/components/src/icons/nft.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { SVGProps } from "react"
import * as React from "react"

export function RiNftFill(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M9 12a2 2 0 1 0 0-4a2 2 0 0 0 0 4m3-11l9.5 5.5v11L12 23l-9.5-5.5v-11zM4.5 7.653v8.694l2.372 1.373l8.073-5.92l4.555 2.734v-6.88L12 3.31z"
/>
</svg>
)
}

0 comments on commit aceb4c9

Please sign in to comment.