fix(wallet): balance align

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-10-24 16:06:21 +08:00
parent cf08c75275
commit 3a35a971f1
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
13 changed files with 27 additions and 751 deletions

View File

@ -27,7 +27,7 @@ export const BoostCertification = ({
<TooltipTrigger asChild>
<i
className={cn(
"i-mgc-rocket-cute-fi ml-1.5 shrink-0 cursor-pointer text-amber-500 hover:bg-amber-400",
"i-mgc-rocket-cute-fi ml-1.5 shrink-0 cursor-button text-theme-accent-500 hover:bg-theme-accent-400",
className,
)}
onClick={(e) => {
@ -38,9 +38,9 @@ export const BoostCertification = ({
</TooltipTrigger>
<TooltipPortal>
<TooltipContent className="px-4 py-2">
<div className="flex items-center text-base">
<i className="i-mgc-rocket-cute-fi mr-2 shrink-0 text-amber-500" />
<TooltipContent className="px-2 py-1">
<div className="flex items-center text-sm">
<i className="i-mgc-rocket-cute-fi mr-2 shrink-0 text-theme-accent-500 animate-rocket" />
{t("boost.feed_being_boosted")}
</div>
</TooltipContent>

View File

@ -149,11 +149,7 @@ export const MyWalletSection = ({ className }: { className?: string }) => {
values={{ blockchainName: "VSL" }}
components={{
Balance: (
<Balance
className="align-sub"
withSuffix
value={BigInt(myWallet.todayDailyPower || 0n)}
>
<Balance withSuffix value={BigInt(myWallet.todayDailyPower || 0n)}>
{BigInt(myWallet.todayDailyPower || 0n)}
</Balance>
),

View File

@ -23,10 +23,12 @@ export const Balance = ({
const formattedFull = format(n, { digits: 18, trailingZeros: true })
const Content = (
<div className={cn("inline-flex items-center gap-1 tabular-nums", className)}>
{withSuffix && <i className="i-mgc-power align-text-bottom text-accent" />}
<span className={cn("tabular-nums", className)}>
{withSuffix && (
<i className="i-mgc-power mr-1 translate-y-[-3px] align-text-bottom text-accent" />
)}
<span>{formatted}</span>
</div>
</span>
)
if (!withTooltip) return Content

View File

@ -1,11 +0,0 @@
import { redirect } from "react-router-dom"
/**
* @deprecated remove after split application
*/
export const loader = async ({ params }: { params: { id: string } }) => {
return redirect(`/share/feeds/${params.id}`)
}
export function Component() {
return null
}

View File

@ -1,105 +0,0 @@
import { FollowIcon } from "@follow/components/icons/follow.jsx"
import { Logo } from "@follow/components/icons/logo.jsx"
import { cn } from "@follow/utils/utils"
import type { MotionValue } from "framer-motion"
import { useMotionValueEvent, useScroll } from "framer-motion"
import type { FC } from "react"
import { cloneElement, useState } from "react"
import { useTranslation } from "react-i18next"
import { Link, Outlet } from "react-router-dom"
import { whoami } from "~/atoms/user"
import { usePresentUserProfileModal } from "~/modules/profile/hooks"
import { UserAvatar } from "~/modules/user/UserAvatar"
const useMotionValueToState = (value: MotionValue<number>) => {
const [state, setState] = useState(value.get())
useMotionValueEvent(value, "change", (v) => setState(v))
return state
}
/** @deprecated */
export function Component() {
return (
<>
<Header />
<main className="flex h-full grow flex-col pt-[80px]">
<Outlet />
</main>
</>
)
}
function Header() {
const present = usePresentUserProfileModal()
const { t } = useTranslation("external")
const { scrollY } = useScroll()
const scrollYState = useMotionValueToState(scrollY)
return (
<header className="fixed inset-x-0 top-0 z-[99] px-5 py-2 xl:px-0">
<div className="relative mx-auto my-4 flex w-full max-w-[1240px] items-center justify-between">
<div
className="absolute inset-0 z-0 -mx-3 -my-2 rounded-full bg-slate-50/80 backdrop-blur-lg dark:bg-neutral-900/80 xl:-mx-8"
style={{
opacity: scrollYState / 100,
}}
/>
<div className="relative z-10 flex w-full items-center justify-between gap-8 px-3">
<div className="flex grow items-center gap-8">
<div className="flex items-center gap-2 text-xl font-bold">
<Logo className="size-8" />
</div>
<div className="mx-6 hidden gap-12 text-sm font-medium lg:flex [&>div]:hover:cursor-pointer">
<HoverableLink
href="/"
icon={<FollowIcon className="!size-3" />}
label={t("header.app")}
/>
<HoverableLink
href="https://github.com/RSSNext/follow/releases"
icon={<i className="i-mgc-download-2-cute-fi" />}
label={t("header.download")}
/>
</div>
</div>
<button
className="cursor-pointer"
type="button"
onClick={() => {
present(whoami()?.id)
}}
>
<UserAvatar className="h-10 bg-transparent p-0" hideName />
</button>
</div>
</div>
</header>
)
}
const HoverableLink: FC<{
label: string
icon: React.JSX.Element
href: string
className?: string
}> = ({ icon, label, href, className }) => (
<Link
to={href}
target={href.startsWith("http") ? "_blank" : undefined}
className={cn("group center flex gap-3 duration-200 hover:text-accent", className)}
>
<span className="center">
{cloneElement(icon, { className: `size-4 ${icon.props.className}` })}
</span>
<span className="inline-flex h-[1.5em] flex-col overflow-hidden leading-[1.5em]">
<span className="inline-flex flex-col gap-2 duration-200 group-hover:translate-y-[calc(-50%-0.25em)]">
<span>{label}</span>
<span>{label}</span>
</span>
</span>
</Link>
)

View File

@ -1,11 +0,0 @@
import { redirect } from "react-router-dom"
export const loader = async ({ params }: { params: { id: string } }) => {
return redirect(`/share/lists/${params.id}`)
}
/**
* @deprecated remove after split application
*/
export function Component() {
return null
}

View File

@ -1,11 +0,0 @@
import { redirect } from "react-router-dom"
export const loader = async ({ params }: { params: { id: string } }) => {
return redirect(`/share/users/${params.id}`)
}
/**
* @deprecated remove after split application
*/
export function Component() {
return null
}

View File

@ -1,139 +0,0 @@
import { FollowIcon } from "@follow/components/icons/follow.jsx"
import { Button } from "@follow/components/ui/button/index.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import type { FeedViewType } from "@follow/constants"
import { views } from "@follow/constants"
import { useTitle } from "@follow/hooks"
import type { FeedModel } from "@follow/models/types"
import { cn } from "@follow/utils/utils"
import type { FC } from "react"
import { useTranslation } from "react-i18next"
import { useParams, useSearchParams } from "react-router-dom"
import { toast } from "sonner"
import { PoweredByFooter } from "~/components/common/PoweredByFooter"
import { usePresentFeedFormModal } from "~/hooks/biz/useFeedFormModal"
import { getItemComponentByView } from "~/modules/entry-column/Items"
import type { UniversalItemProps } from "~/modules/entry-column/types"
import { FeedCertification } from "~/modules/feed/feed-certification"
import { FeedIcon } from "~/modules/feed/feed-icon"
import { useEntriesPreview } from "~/queries/entries"
import { useFeed } from "~/queries/feed"
export function Component() {
const { id } = useParams()
const [search] = useSearchParams()
const view = Number.parseInt(search.get("view") || "0")
const feed = useFeed({
id,
})
const feedData = feed.data?.feed as FeedModel
const isSubscribed = !!feed.data?.subscription
const entries = useEntriesPreview({
id,
})
const Item: FC<UniversalItemProps> = getItemComponentByView(view as FeedViewType)
const { t } = useTranslation("external")
useTitle(feed.data?.feed.title)
const presentFeedFormModal = usePresentFeedFormModal()
return (
<>
{feed.isLoading ? (
<LoadingCircle size="large" className="center fixed inset-0" />
) : (
feed.data?.feed && (
<div className="mx-auto mt-12 flex w-full max-w-5xl flex-col items-center justify-center p-4 lg:p-0">
<FeedIcon
fallback
feed={feed.data.feed}
className="mask-squircle mask shrink-0"
size={64}
/>
<div className="flex flex-col items-center">
<div className="mb-2 mt-4 flex items-center text-2xl font-bold">
<h1>{feed.data.feed.title}</h1>
<FeedCertification feed={feed.data.feed} />
</div>
<div className="mb-8 text-sm text-zinc-500">{feed.data.feed.description}</div>
</div>
<div className="mb-4 text-sm">
{t("feed.followsAndReads", {
subscriptionCount: feed.data.subscriptionCount,
subscriptionNoun: t("feed.follower", { count: feed.data.subscriptionCount }),
readCount: feed.data.readCount,
readNoun: t("feed.read", { count: feed.data.readCount }),
appName: APP_NAME,
})}
</div>
<span className="center mb-8 flex gap-4">
{feedData.url.startsWith("https://") ? (
<Button
variant={"outline"}
onClick={() => {
window.open(feedData.url, "_blank")
}}
>
{t("feed.view_feed_url")}
</Button>
) : (
<Button
variant={"outline"}
onClick={() => {
toast.success(t("copied_link"))
navigator.clipboard.writeText(feedData.url)
}}
>
{t("feed.copy_feed_url")}
</Button>
)}
<Button
variant={isSubscribed ? "outline" : undefined}
onClick={() => {
presentFeedFormModal({
feedId: id!,
})
}}
>
<FollowIcon className="mr-1 size-3" />
{isSubscribed ? t("feed.actions.followed") : <>{APP_NAME}</>}
</Button>
</span>
<div
className={cn(
"w-full pb-12 pt-8",
views[view].gridMode
? "grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4"
: "flex max-w-3xl flex-col gap-6",
)}
>
{entries.data?.map((entry) => (
<a
className="relative cursor-pointer"
href={entry.url || void 0}
target="_blank"
key={entry.id}
>
<div className="rounded-xl pl-3 duration-300 hover:bg-theme-item-active">
<Item
entryPreview={{
entries: entry,
feeds: feedData,
read: true,
feedId: feedData.id!,
}}
entryId={entry.id}
/>
</div>
</a>
))}
</div>
<PoweredByFooter className="pb-12" />
</div>
)
)}
</>
)
}

View File

@ -1,122 +0,0 @@
import { FollowIcon } from "@follow/components/icons/follow.jsx"
import { Button } from "@follow/components/ui/button/index.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import { useTitle } from "@follow/hooks"
import type { ListModel } from "@follow/models/types"
import { useTranslation } from "react-i18next"
import { useParams } from "react-router-dom"
import { toast } from "sonner"
import { PoweredByFooter } from "~/components/common/PoweredByFooter"
import { usePresentFeedFormModal } from "~/hooks/biz/useFeedFormModal"
import { FeedCertification } from "~/modules/feed/feed-certification"
import { FeedIcon } from "~/modules/feed/feed-icon"
import { useList } from "~/queries/lists"
import { useFeedById } from "~/store/feed/hooks"
export function Component() {
const { id } = useParams()
const list = useList({
id: id!,
})
const listData = list.data?.list as ListModel
const isSubscribed = !!list.data?.subscription
const { t } = useTranslation("external")
useTitle(list.data?.list.title)
const presentFeedFormModal = usePresentFeedFormModal()
return (
<>
{list.isLoading ? (
<LoadingCircle size="large" className="center fixed inset-0" />
) : (
list.data?.list && (
<div className="mx-auto mt-12 flex w-full max-w-5xl flex-col items-center justify-center p-4 lg:p-0">
<FeedIcon
fallback
feed={list.data.list}
className="mask-squircle mask shrink-0"
size={64}
/>
<div className="flex flex-col items-center">
<div className="mb-2 mt-4 flex items-center text-2xl font-bold">
<h1>{list.data.list.title}</h1>
<FeedCertification feed={list.data.list} />
</div>
<div className="mb-8 text-sm text-zinc-500">{list.data.list.description}</div>
</div>
<div className="mb-4 text-sm">
{t("feed.followsAndFeeds", {
subscriptionCount: list.data?.subscriptionCount,
subscriptionNoun: t("feed.follower", { count: list.data?.subscriptionCount }),
feedsCount: list.data?.feedCount || 0,
feedsNoun: t("feed.feeds", { count: listData?.feedIds?.length }),
appName: APP_NAME,
})}
</div>
<span className="center mb-8 flex gap-4">
<Button
variant={"outline"}
onClick={() => {
toast.success(t("copied_link"))
navigator.clipboard.writeText(id!)
}}
>
Copy List ID
</Button>
<Button
variant={isSubscribed ? "outline" : undefined}
onClick={() => {
presentFeedFormModal({
listId: id!,
})
}}
>
<FollowIcon className="mr-2 size-3" />
{isSubscribed ? "Followed" : <>{APP_NAME}</>}
</Button>
</span>
<div className="flex w-full max-w-3xl flex-col gap-4 pb-12 pt-8">
{listData.feedIds
?.slice(0, 5)
.map((feedId) => <FeedRow feedId={feedId} key={feedId} />)}
{"feedCount" in list.data && (
<div
onClick={() => {
presentFeedFormModal({
listId: id!,
})
}}
className="text-sm text-zinc-500"
>
{t("feed.follow_to_view_all", {
count: list.data.feedCount || 0,
})}
</div>
)}
</div>
<PoweredByFooter className="mt-20 pb-12" />
</div>
)
)}
</>
)
}
const FeedRow = ({ feedId }: { feedId: string }) => {
const feed = useFeedById(feedId)
if (!feed) return null
return (
<a
className="relative flex cursor-pointer items-center text-base"
href={`/share/feeds/${feed.id}`}
target="_blank"
key={feed.id}
>
<FeedIcon fallback feed={feed} className="mask-squircle mask mr-2 shrink-0" size={20} />
{feed.title}
<FeedCertification feed={feed} />
</a>
)
}

View File

@ -1,143 +0,0 @@
import { FollowIcon } from "@follow/components/icons/follow.jsx"
import { Avatar, AvatarFallback, AvatarImage } from "@follow/components/ui/avatar/index.jsx"
import { Button } from "@follow/components/ui/button/index.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import { useTitle } from "@follow/hooks"
import { UrlBuilder } from "@follow/utils/url-builder"
import { cn, isBizId } from "@follow/utils/utils"
import { Fragment } from "react/jsx-runtime"
import { useParams } from "react-router-dom"
import { useWhoami } from "~/atoms/user"
import { PoweredByFooter } from "~/components/common/PoweredByFooter"
import { usePresentFeedFormModal } from "~/hooks/biz/useFeedFormModal"
import { useAuthQuery, useI18n } from "~/hooks/common"
import { apiClient } from "~/lib/api-fetch"
import { defineQuery } from "~/lib/defineQuery"
import { replaceImgUrlIfNeed } from "~/lib/img-proxy"
import { FeedIcon } from "~/modules/feed/feed-icon"
import { useUserSubscriptionsQuery } from "~/modules/profile/hooks"
export function Component() {
const t = useI18n()
const { id } = useParams()
const isHandle = id ? id.startsWith("@") || !isBizId(id) : false
const user = useAuthQuery(
defineQuery(["profiles", id], async () => {
const res = await apiClient.profiles.$get({
query: {
handle: isHandle ? (id?.startsWith("@") ? id.slice(1) : id) : undefined,
id: isHandle ? undefined : id,
},
})
return res.data
}),
{
enabled: !!id,
},
)
const subscriptions = useUserSubscriptionsQuery(user.data?.id)
useTitle(user.data?.name)
const me = useWhoami()
const isMe = user.data?.id === me?.id
const presentFeedFormModal = usePresentFeedFormModal()
return (
<div className="container mx-auto mt-12 flex grow flex-col items-center p-4 lg:p-0">
{user.isLoading ? (
<LoadingCircle size="large" className="center fixed inset-0" />
) : (
<Fragment>
<Avatar className="aspect-square size-16">
<AvatarImage
className="duration-200 animate-in fade-in-0"
src={replaceImgUrlIfNeed(user.data?.image || undefined)}
/>
<AvatarFallback>{user.data?.name?.slice(0, 2)}</AvatarFallback>
</Avatar>
<div className="flex flex-col items-center">
<div className="mb-2 mt-4 flex items-center text-2xl font-bold">
<h1>{user.data?.name}</h1>
</div>
<div className="mb-8 text-sm text-zinc-500">{user.data?.handle}</div>
</div>
<div
className="mb-12 w-[70ch] max-w-full grow space-y-10"
data-testid="profile-subscriptions"
>
{subscriptions.isLoading ? (
<LoadingCircle size="large" className="center fixed inset-0" />
) : (
Object.keys(subscriptions.data || {}).map((category) => (
<div key={category}>
<div className="mb-4 flex items-center text-2xl font-bold">
<h3>{category}</h3>
</div>
<div>
{subscriptions.data?.[category].map(
(subscription) =>
"feeds" in subscription && (
<div key={subscription.feedId} className="group relative border-b py-5">
<a
className="flex flex-1 cursor-button"
href={UrlBuilder.shareFeed(subscription.feedId)}
target="_blank"
>
<FeedIcon
fallback
feed={subscription.feeds}
size={22}
className="mr-3"
/>
<div
className={cn(
"flex w-0 flex-1 grow flex-col justify-center",
"group-hover:grow-[0.85]",
)}
>
<div className="truncate font-medium leading-none">
{subscription.feeds?.title}
</div>
{subscription.feeds?.description && (
<div className="mt-1 line-clamp-1 text-xs text-zinc-500">
{subscription.feeds.description}
</div>
)}
</div>
</a>
<span className="center absolute inset-y-0 right-0 opacity-0 transition-opacity group-hover:opacity-100">
<Button
onClick={(e) => {
e.stopPropagation()
presentFeedFormModal({
feedId: subscription.feedId,
})
}}
>
{isMe ? (
t.common("words.edit")
) : (
<>
<FollowIcon className="mr-1 size-3" />
{APP_NAME}
</>
)}
</Button>
</span>
</div>
),
)}
</div>
</div>
))
)}
</div>
<PoweredByFooter className="pb-12" />
</Fragment>
)}
</div>
)
}

View File

@ -1,196 +0,0 @@
import { Logo } from "@follow/components/icons/logo.jsx"
import { Button } from "@follow/components/ui/button/index.js"
import { styledButtonVariant } from "@follow/components/ui/button/variants.js"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@follow/components/ui/form/index.jsx"
import { Input } from "@follow/components/ui/input/index.js"
import { Tooltip, TooltipContent, TooltipTrigger } from "@follow/components/ui/tooltip/index.jsx"
import { IN_ELECTRON } from "@follow/shared/constants"
import { zodResolver } from "@hookform/resolvers/zod"
import type { DotLottie } from "@lottiefiles/dotlottie-react"
import { DotLottieReact } from "@lottiefiles/dotlottie-react"
import { m } from "framer-motion"
import type { RefCallback } from "react"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { z } from "zod"
import { SocialMediaLinks } from "~/constants/social"
import { useSignOut } from "~/hooks/biz/useSignOut"
import { tipcClient } from "~/lib/client"
import { getFetchErrorMessage } from "~/lib/error-parser"
import confettiUrl from "~/lottie/confetti.lottie?url"
import { useInvitationMutation } from "~/queries/invitations"
const absoluteConfettiUrl = new URL(confettiUrl, import.meta.url).href
const formSchema = z.object({
code: z.string().min(1),
})
/**
*
* @deprecated
*/
export function Component() {
const { t } = useTranslation("external")
const navigate = useNavigate()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
})
const invitationMutation = useInvitationMutation({
onError(error) {
const message = getFetchErrorMessage(error)
form.setError("code", { message })
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
invitationMutation.mutate(values.code)
}
const [showConfetti, setShowConfetti] = useState(false)
useEffect(() => {
if (invitationMutation.isSuccess) {
setShowConfetti(true)
}
}, [invitationMutation.isSuccess])
const [dotLottie, setDotLottie] = useState<DotLottie | null>(null)
useEffect(() => {
function onComplete() {
navigate("/")
}
if (dotLottie) {
dotLottie.addEventListener("complete", onComplete)
}
return () => {
if (dotLottie) {
dotLottie.removeEventListener("complete", onComplete)
}
}
}, [dotLottie, navigate])
const dotLottieRefCallback: RefCallback<DotLottie> = (dotLottie) => {
setDotLottie(dotLottie)
}
const signOut = useSignOut()
return (
<div className="container flex h-screen w-full flex-col items-center justify-center gap-14">
<Logo className="size-20" />
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-[512px] max-w-full">
<FormField
control={form.control}
name="code"
render={({ field }) => (
<FormItem className="flex flex-col items-center gap-2 md:block">
<FormLabel>{t("invitation.title")}</FormLabel>
<FormControl>
<Input autoFocus {...field} />
</FormControl>
<div className="h-6">
<FormMessage />
</div>
</FormItem>
)}
/>
<div className="center relative flex">
{showConfetti && (
<DotLottieReact
className="absolute size-[120px]"
src={absoluteConfettiUrl}
loop={false}
autoplay
dotLottieRefCallback={dotLottieRefCallback}
/>
)}
<Button
variant={showConfetti ? "outline" : "primary"}
type="submit"
disabled={!form.formState.isValid}
isLoading={invitationMutation.isPending}
>
{showConfetti && (
<m.i
initial={{ y: 5 }}
animate={{
y: 0,
}}
className="i-mgc-check-circle-filled mr-2 text-green-500"
/>
)}
{t("invitation.activate")}
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
buttonClassName="absolute -right-2 -top-1 text-foreground/80"
className="size-8 text-lg"
variant="ghost"
type="button"
onClick={() => {
if (IN_ELECTRON) {
tipcClient?.clearAllData().then(() => {
window.location.href = "/"
})
} else {
signOut()
}
}}
>
<i className="i-mingcute-exit-door-line" />
</Button>
</TooltipTrigger>
<TooltipContent>{t("login.signOut")}</TooltipContent>
</Tooltip>
</div>
</form>
</Form>
<div className="space-y-2 text-balance text-center text-sm text-zinc-600 md:text-left">
<p>{t("invitation.earlyAccessMessage")}</p>
<p>{t("invitation.getCodeMessage")}</p>
<p>
<p>1. {t("invitation.codeOptions.1")}</p>
<p>2. {t("invitation.codeOptions.2")}</p>
<p>3. {t("invitation.codeOptions.3")}</p>
</p>
</div>
<div className="mt-2 flex flex-wrap gap-2">
{SocialMediaLinks.map((link) => (
<span
key={link.url}
className={styledButtonVariant({
variant: "outline",
className: "flex-1",
})}
>
<a
href={link.url}
className="center flex w-full gap-1"
target="_blank"
rel="noreferrer"
>
<i className={link.icon} />
{link.label}
</a>
</span>
))}
</div>
</div>
)
}

View File

@ -378,4 +378,20 @@
transform: rotateY(180deg);
}
}
@keyframes rocketAnimation {
0% {
transform: translateY(0);
}
50% {
transform: translate(2px, -1px);
}
100% {
transform: translateY(0);
}
}
.animate-rocket {
animation: rocketAnimation 1s infinite ease-out;
}
}

View File

@ -60,7 +60,7 @@ export const styledButtonVariant = cva(
text: cn(
"font-semibold text-accent",
"hover:contrast-[1.10] active:contrast-125",
"focus:text-accent focus:outline-none",
tw`focus:text-accent focus:outline-none p-0 inline align-baseline`,
),
ghost: cn("px-1.5 font-semibold", "hover:bg-theme-button-hover"),
},