fix(wallet): balance align
Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
cf08c75275
commit
3a35a971f1
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue