feat: loading component new design

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-08-25 21:19:19 +08:00
parent 53a5bae316
commit 91d0d76efe
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
8 changed files with 206 additions and 42 deletions

View File

@ -24,6 +24,7 @@ export default defineConfig(
rules: {
"unicorn/prefer-math-trunc": "off",
"@eslint-react/no-clone-element": 0,
"no-restricted-syntax": 0,
"no-restricted-globals": [
"error",
{

View File

@ -1,19 +1,111 @@
import { cn } from "@renderer/lib/utils"
import React, { cloneElement } from "react"
interface LoadingCircleProps {
size: "small" | "medium" | "large"
}
const sizeMap = {
small: "text-md",
medium: "text-xl",
large: "text-3xl",
small: 16,
medium: 24,
large: 30,
}
export const LoadingCircle: Component<LoadingCircleProps> = ({
className,
size,
}) => (
<div className={cn(sizeMap[size], className)}>
<div
className={className}
style={{
fontSize: sizeMap[size],
}}
>
<i className="i-mgc-loading-3-cute-re animate-spin" />
</div>
)
const smallIconSizeMap = {
small: 12,
medium: 14,
large: 16,
}
export const LoadingWithIcon: Component<
LoadingCircleProps & {
icon: React.JSX.Element
order?: "loading-first" | "icon-first"
}
> = ({ order = "loading-first", size, className, icon, children }) => {
const rootStyle = { width: sizeMap[size], height: sizeMap[size] }
const smallIconStyle = {
height: smallIconSizeMap[size],
width: smallIconSizeMap[size],
}
const Children = children && <div className="center flex">{children}</div>
if (order === "icon-first") {
return (
<div className={className}>
<div style={rootStyle} className="relative inline-block">
<span className="block size-full">
{cloneElement(icon, {
style: {
...icon.props.style,
fontSize: sizeMap[size],
},
className: cn(icon.props.className),
})}
</span>
<span
className="absolute bottom-1"
style={{
...smallIconStyle,
right: -smallIconSizeMap[size] + 4,
}}
>
<i
className="i-mgc-loading-3-cute-re animate-spin"
style={{ fontSize: smallIconSizeMap[size] }}
/>
</span>
</div>
{Children}
</div>
)
} else {
return (
<div className={className}>
<div style={rootStyle} className="relative inline-block">
<span
className="block size-full"
style={{
clipPath: `polygon(0% 0%, 0% 100%, ${smallIconSizeMap[size]}px 100%, ${smallIconSizeMap[size]}px ${smallIconSizeMap[size]}px, 100% ${smallIconSizeMap[size]}px, 100% 100%, 100% 100%, 100% 0%)`,
}}
>
<i
className="i-mgc-loading-3-cute-re animate-spin"
style={{ fontSize: sizeMap[size] }}
/>
</span>
<span
className={cn(
"absolute bottom-0 right-0",
"animate-pulse duration-700",
)}
style={smallIconStyle}
>
{cloneElement(icon, {
style: {
...icon.props.style,
fontSize: smallIconSizeMap[size],
},
className: icon.props.className,
})}
</span>
</div>
{Children}
</div>
)
}
}

View File

@ -32,7 +32,7 @@ import { useFeedById, useFeedHeaderTitle } from "@renderer/store/feed"
import type { FC } from "react"
import { useEffect, useLayoutEffect, useRef } from "react"
import { LoadingCircle } from "../../components/ui/loading"
import { LoadingWithIcon } from "../../components/ui/loading"
import { EntryPlaceholderDaily } from "../ai/ai-daily/EntryPlaceholderDaily"
import { EntryTranslation } from "../entry-column/translation"
import { setEntryContentScrollToTop, setEntryTitleMeta } from "./atoms"
@ -235,7 +235,12 @@ export const EntryContentRender: Component<{ entryId: string }> = ({
{!content && (
<div className="center mt-16">
{isPending ? (
<LoadingCircle size="large" />
<LoadingWithIcon
size="large"
icon={
<i className="i-mgc-rss-cute-fi" />
}
/>
) : error ?
(
<div className="center flex flex-col gap-2">
@ -308,7 +313,10 @@ const ReadabilityContent = ({ entryId }: { entryId: string }) => {
</p>
) : (
<div className="center mt-16 flex flex-col gap-2">
<LoadingCircle size="large" />
<LoadingWithIcon
size="large"
icon={<i className="i-mgc-sparkles-2-cute-re" />}
/>
<span className="text-sm">
Fetching original content and processing...
</span>

View File

@ -8,7 +8,10 @@ import {
AvatarImage,
} from "@renderer/components/ui/avatar"
import { ActionButton, Button } from "@renderer/components/ui/button"
import { LoadingCircle } from "@renderer/components/ui/loading"
import {
LoadingCircle,
LoadingWithIcon,
} from "@renderer/components/ui/loading"
import { useCurrentModal, useModalStack } from "@renderer/components/ui/modal"
import { ScrollArea } from "@renderer/components/ui/scroll-area"
import { useAuthQuery } from "@renderer/hooks/common"
@ -277,8 +280,9 @@ export const UserProfileModalContent: FC<{
viewportClassName="[&>div]:space-y-4 pb-4"
>
{subscriptions.isLoading ? (
<LoadingCircle
<LoadingWithIcon
size="large"
icon={<i className="i-mgc-user-3-cute-re" />}
className="center h-48 w-full max-w-full"
/>
) : (

View File

@ -1,4 +1,5 @@
/* eslint-disable @eslint-react/no-array-index-key */
import { isNil } from "lodash-es"
import type { FC, ReactNode } from "react"
import * as React from "react"
import { isValidElement } from "react"
@ -49,7 +50,7 @@ export const createSettingBuilder =
const settingObject = useSetting()
return settings
.filter((i) => typeof i !== "boolean")
.filter((i) => !isNil(i))
.map((setting, index) => {
if (isValidElement(setting)) return setting
if (typeof setting === "function") {

View File

@ -1,6 +1,6 @@
import { useWhoami } from "@renderer/atoms/user"
import { Divider } from "@renderer/components/ui/divider"
import { LoadingCircle } from "@renderer/components/ui/loading"
import { LoadingWithIcon } from "@renderer/components/ui/loading"
import {
Tooltip,
TooltipContent,
@ -22,7 +22,11 @@ export const MyWalletSection = () => {
if (wallet.isPending) {
return (
<div className="center absolute inset-0 flex">
<LoadingCircle size="large" className="-translate-y-full" />
<LoadingWithIcon
icon={<i className="i-mgc-power" />}
size="large"
className="-translate-y-full"
/>
</div>
)
}
@ -63,9 +67,7 @@ export const MyWalletSection = () => {
<TooltipTrigger className="block">
<div className="flex flex-row items-center gap-x-2 text-xs text-zinc-600 dark:text-neutral-400">
<span className="flex w-[120px] items-center gap-1 text-left">
Cashable Power
{" "}
<i className="i-mingcute-question-line" />
Cashable Power <i className="i-mingcute-question-line" />
</span>
<Balance>{myWallet.cashablePowerToken}</Balance>
@ -77,8 +79,8 @@ export const MyWalletSection = () => {
1. Cashable Power can be withdrawn to your wallet for trading.
</p>
<p>
2. Cashable Power is the Power you have recharged and the tips you
have received.
2. Cashable Power is the Power you have recharged and the tips
you have received.
</p>
</TooltipContent>
</TooltipPortal>

View File

@ -53,10 +53,10 @@ export const TransactionsSection = () => {
<TableHead className="text-center" size="sm">
Amount
</TableHead>
<TableHead className="text-center" size="sm">
<TableHead className="pl-5" size="sm">
From
</TableHead>
<TableHead className="text-center" size="sm">
<TableHead className="pl-5" size="sm">
To
</TableHead>
<TableHead className="text-center" size="sm">
@ -152,7 +152,7 @@ const UserRenderer = ({
const name = isMe ? "You" : user?.name || APP_NAME
return (
<div className="center">
<div className="flex">
{name === APP_NAME ? (
<Logo className="aspect-square size-4" />
) : (

View File

@ -1,14 +1,20 @@
import { useWhoami } from "@renderer/atoms/user"
import { Button } from "@renderer/components/ui/button"
import { Divider } from "@renderer/components/ui/divider"
import { LoadingCircle } from "@renderer/components/ui/loading"
import {
LoadingWithIcon,
} from "@renderer/components/ui/loading"
import { useCurrentModal } from "@renderer/components/ui/modal"
import { RadioGroup } from "@renderer/components/ui/radio-group"
import { RadioCard } from "@renderer/components/ui/radio-group/RadioCard"
import { Balance } from "@renderer/components/ui/wallet/balance"
import { UserAvatar } from "@renderer/components/user-button"
import { nextFrame } from "@renderer/lib/dom"
import { useWallet, useWalletTipMutation, useWalletTransactions } from "@renderer/queries/wallet"
import {
useWallet,
useWalletTipMutation,
useWalletTransactions,
} from "@renderer/queries/wallet"
import { from, toNumber } from "dnum"
import type { FC } from "react"
import { useState } from "react"
@ -18,28 +24,63 @@ import { useSettingModal } from "../settings/modal/hooks-hack"
const DEFAULT_RECOMMENDED_TIP = 1
const useMyWallet = () => {
const user = useWhoami()
const myWallet = useWallet({ userId: user?.id })
return myWallet
}
const Loading = () => (
<div className="center h-32 w-[350px]">
<LoadingWithIcon icon={<i className="i-mgc-power" />} size="large" />
</div>
)
export const TipModalContent: FC<{
userId?: string
feedId?: string
}> = (props) => {
const myWallet = useMyWallet()
if (!myWallet.data) {
return (
<Loading />
)
}
return <TipModalContent_ {...props} />
}
const TipModalContent_: FC<{
userId?: string
feedId?: string
}> = ({ userId, feedId }) => {
const user = useWhoami()
const myWallet = useWallet({ userId: user?.id })
const myWallet = useMyWallet()
const myWalletData = myWallet.data?.[0]
const dPowerBigInt = BigInt(myWalletData?.dailyPowerToken ?? 0)
const cPowerBigInt = BigInt(myWalletData?.cashablePowerToken ?? 0)
const balanceBigInt = cPowerBigInt + dPowerBigInt
const balanceNumber = toNumber(from(balanceBigInt, 18), { digits: 2, trailingZeros: true })
const balanceNumber = toNumber(from(balanceBigInt, 18), {
digits: 2,
trailingZeros: true,
})
const transactionsQuery = useWalletTransactions({ toUserId: userId, toFeedId: feedId })
const transactionsQuery = useWalletTransactions({
toUserId: userId,
toFeedId: feedId,
})
const tipMutation = useWalletTipMutation()
const [amount, setAmount] = useState(DEFAULT_RECOMMENDED_TIP > balanceNumber ? balanceNumber : DEFAULT_RECOMMENDED_TIP)
const [amount, setAmount] = useState(
DEFAULT_RECOMMENDED_TIP > balanceNumber ?
balanceNumber :
DEFAULT_RECOMMENDED_TIP,
)
const amountBigInt = from(amount, 18)[0]
const wrongNumberRange = amountBigInt > balanceBigInt || amountBigInt <= BigInt(0)
const wrongNumberRange =
amountBigInt > balanceBigInt || amountBigInt <= BigInt(0)
const { dismiss } = useCurrentModal()
@ -51,9 +92,7 @@ export const TipModalContent: FC<{
if (transactionsQuery.isPending || myWallet.isPending) {
return (
<div className="center h-32 w-[350px]">
<LoadingCircle size="large" />
</div>
<Loading />
)
}
@ -88,14 +127,12 @@ export const TipModalContent: FC<{
<p>
<Balance withSuffix>{amountBigInt}</Balance>
{" "}
has been sent to the author.
has been sent to the
author.
</p>
<div className="flex justify-end">
<Button
variant="primary"
onClick={() => dismiss()}
>
<Button variant="primary" onClick={() => dismiss()}>
OK
</Button>
</div>
@ -108,15 +145,27 @@ export const TipModalContent: FC<{
{userId ? (
<>
<p className="text-sm font-medium">Feed Owner</p>
<UserAvatar className="h-8 justify-start bg-transparent p-0" userId={userId} />
<UserAvatar
className="h-8 justify-start bg-transparent p-0"
userId={userId}
/>
</>
) : (
<>
<p className="leading-none">
<span className="text-xs text-theme-foreground/80">No one has claimed this feed yet. The received Power will be securely held in the blockchain contract until it is claimed.</span>
<span className="text-xs text-theme-foreground/80">
No one has claimed this feed yet. The received Power will be
securely held in the blockchain contract until it is claimed.
</span>
</p>
<div className="text-center">
<Button variant="text" className="w-fit p-0" onClick={() => claimFeed()}>Claim this feed</Button>
<Button
variant="text"
className="w-fit p-0"
onClick={() => claimFeed()}
>
Claim this feed
</Button>
</div>
</>
)}
@ -136,8 +185,16 @@ export const TipModalContent: FC<{
onValueChange={(value) => setAmount(Number(value))}
>
<div className="grid grid-cols-2 gap-2">
<RadioCard wrapperClassName="justify-center" label="1 Power" value="1" />
<RadioCard wrapperClassName="justify-center" label="2 Power" value="2" />
<RadioCard
wrapperClassName="justify-center"
label="1 Power"
value="1"
/>
<RadioCard
wrapperClassName="justify-center"
label="2 Power"
value="2"
/>
</div>
</RadioGroup>
@ -152,7 +209,6 @@ export const TipModalContent: FC<{
</span>
</div>
</>
)}
</div>