Implement Patron in FE
This commit is contained in:
@ -18,7 +18,9 @@ import { isTimezoneLabel } from '@/features/ui/components/profile-field';
|
||||
import { UserPanel } from '@/features/ui/util/async-components';
|
||||
import { useAppDispatch } from '@/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from '@/hooks/use-app-selector';
|
||||
import { useFrontendConfig } from '@/hooks/use-frontend-config';
|
||||
import { accountScrobbleQueryOptions } from '@/queries/accounts/account-scrobble';
|
||||
import { usePatronUser } from '@/queries/patron/use-patron-user';
|
||||
import { useAccountHoverCardActions, useAccountHoverCardStore } from '@/stores/account-hover-card';
|
||||
|
||||
import AccountLocalTime from './account-local-time';
|
||||
@ -35,6 +37,7 @@ const messages = {
|
||||
|
||||
const getBadges = (
|
||||
account?: Pick<Account, 'is_admin' | 'is_moderator'>,
|
||||
isPatron?: boolean,
|
||||
): JSX.Element[] => {
|
||||
const badges = [];
|
||||
|
||||
@ -44,6 +47,10 @@ const getBadges = (
|
||||
badges.push(<Badge key='moderator' slug='moderator' title={<FormattedMessage id='account_moderation_modal.roles.moderator' defaultMessage='Moderator' />} />);
|
||||
}
|
||||
|
||||
if (isPatron) {
|
||||
badges.push(<Badge key='patron' slug='patron' title={<FormattedMessage id='account.patron' defaultMessage='Patron' />} />);
|
||||
}
|
||||
|
||||
return badges;
|
||||
};
|
||||
|
||||
@ -61,9 +68,11 @@ const AccountHoverCard: React.FC<IAccountHoverCard> = ({ visible = true }) => {
|
||||
const { updateAccountHoverCard, closeAccountHoverCard } = useAccountHoverCardActions();
|
||||
|
||||
const me = useAppSelector(state => state.me);
|
||||
const frontendConfig = useFrontendConfig();
|
||||
const { account } = useAccount(accountId || undefined, { withRelationship: true });
|
||||
const { data: scrobble } = useQuery(accountScrobbleQueryOptions(account?.id));
|
||||
const badges = getBadges(account);
|
||||
const { data: patronUser } = usePatronUser(frontendConfig.patron.enabled ? account?.url : undefined);
|
||||
const badges = getBadges(account, patronUser?.is_patron);
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId) dispatch(fetchRelationships([accountId]));
|
||||
@ -146,6 +155,7 @@ const AccountHoverCard: React.FC<IAccountHoverCard> = ({ visible = true }) => {
|
||||
accountId={account.id}
|
||||
action={<ActionButton account={account} small />}
|
||||
badges={badges}
|
||||
isPatron={patronUser?.is_patron}
|
||||
/>
|
||||
|
||||
{account.local ? (
|
||||
|
||||
@ -4,6 +4,7 @@ import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import HoverAccountWrapper from '@/components/hover-account-wrapper';
|
||||
import PatronIndicator from '@/components/patron-indicator';
|
||||
import Avatar from '@/components/ui/avatar';
|
||||
import Emoji from '@/components/ui/emoji';
|
||||
import HStack from '@/components/ui/hstack';
|
||||
@ -16,6 +17,8 @@ import Emojify from '@/features/emoji/emojify';
|
||||
import ActionButton from '@/features/ui/components/action-button';
|
||||
import { useAcct } from '@/hooks/use-acct';
|
||||
import { useAppSelector } from '@/hooks/use-app-selector';
|
||||
import { useFrontendConfig } from '@/hooks/use-frontend-config';
|
||||
import { usePatronUser } from '@/queries/patron/use-patron-user';
|
||||
import { useSettings } from '@/stores/settings';
|
||||
|
||||
import Badge from './badge';
|
||||
@ -156,6 +159,8 @@ const Account = ({
|
||||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
const username = useAcct(account);
|
||||
const frontendConfig = useFrontendConfig();
|
||||
const { data: patronUser } = usePatronUser(frontendConfig.patron.enabled ? account.url : undefined);
|
||||
const { disableUserProvidedMedia } = useSettings();
|
||||
|
||||
const handleAction = () => {
|
||||
@ -241,7 +246,9 @@ const Account = ({
|
||||
<Avatar src={account.avatar} alt={account.avatar_description} username={account.username} />
|
||||
) : (
|
||||
<div className='rounded-lg'>
|
||||
<PatronIndicator isPatron={patronUser?.is_patron}>
|
||||
<Avatar src={account.avatar} size={avatarSize} alt={account.avatar_description} isCat={account.is_cat} username={account.username} />
|
||||
</PatronIndicator>
|
||||
{emoji && (
|
||||
<Emoji
|
||||
className='!absolute -right-1.5 bottom-0 size-5'
|
||||
@ -310,7 +317,9 @@ const Account = ({
|
||||
wrapper={(children) => <HoverAccountWrapper className='relative' accountId={account.id} element='span'>{children}</HoverAccountWrapper>}
|
||||
>
|
||||
<LinkEl className='rounded-lg' {...linkProps}>
|
||||
<PatronIndicator isPatron={patronUser?.is_patron}>
|
||||
<Avatar src={account.avatar} size={avatarSize} alt={account.avatar_description} isCat={account.is_cat} username={account.username} />
|
||||
</PatronIndicator>
|
||||
{emoji && (
|
||||
<Emoji
|
||||
className='!absolute -right-1.5 bottom-0 size-5'
|
||||
|
||||
@ -13,9 +13,11 @@ import Icon from '@/components/ui/icon';
|
||||
import Stack from '@/components/ui/stack';
|
||||
import Text from '@/components/ui/text';
|
||||
import ProfileStats from '@/features/ui/components/profile-stats';
|
||||
import { FundingPanel } from '@/features/ui/util/async-components';
|
||||
import { useAppDispatch } from '@/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from '@/hooks/use-app-selector';
|
||||
import { useFeatures } from '@/hooks/use-features';
|
||||
import { useFrontendConfig } from '@/hooks/use-frontend-config';
|
||||
import { useInstance } from '@/hooks/use-instance';
|
||||
import { useRegistrationStatus } from '@/hooks/use-registration-status';
|
||||
import { useFollowRequestsCount } from '@/queries/accounts/use-follow-requests';
|
||||
@ -92,6 +94,7 @@ const DropdownNavigation: React.FC = React.memo((): JSX.Element | null => {
|
||||
|
||||
const instance = useInstance();
|
||||
const restrictUnauth = instance.pleroma.metadata.restrict_unauthenticated;
|
||||
const frontendConfig = useFrontendConfig();
|
||||
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -195,6 +198,13 @@ const DropdownNavigation: React.FC = React.memo((): JSX.Element | null => {
|
||||
<Stack space={4}>
|
||||
<Divider />
|
||||
|
||||
{frontendConfig.patron.enabled && (
|
||||
<>
|
||||
<FundingPanel compact />
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownNavigationLink
|
||||
to='/@{$username}'
|
||||
params={{ username: account.acct }}
|
||||
|
||||
17
packages/pl-fe/src/components/patron-indicator.tsx
Normal file
17
packages/pl-fe/src/components/patron-indicator.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
interface IPatronIndicator {
|
||||
isPatron?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PatronIndicator: React.FC<IPatronIndicator> = ({ isPatron, children }) => {
|
||||
if (!isPatron) return <>{children}</>;
|
||||
return (
|
||||
<div className='rounded-lg bg-gradient-to-r from-blue-900 to-purple-500 p-[2px]' title='Patron'>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { PatronIndicator as default };
|
||||
@ -16,9 +16,7 @@ const ProgressBar: React.FC<IProgressBar> = ({ progress, size = 'md' }) => {
|
||||
const { reduceMotion } = useSettings();
|
||||
|
||||
const styles = useSpring({
|
||||
from: { width: '0%' },
|
||||
to: { width: `${progress}%` },
|
||||
reset: true,
|
||||
to: { width: `${Math.min(progress * 100, 100)}%` },
|
||||
immediate: reduceMotion,
|
||||
});
|
||||
|
||||
@ -30,9 +28,13 @@ const ProgressBar: React.FC<IProgressBar> = ({ progress, size = 'md' }) => {
|
||||
})}
|
||||
>
|
||||
<animated.div
|
||||
className='h-full bg-secondary-500'
|
||||
className='relative h-full overflow-hidden bg-gradient-to-r from-blue-900 to-purple-500'
|
||||
style={styles}
|
||||
/>
|
||||
>
|
||||
{!reduceMotion && (
|
||||
<div className='absolute inset-0 animate-shimmer bg-gradient-to-r from-transparent via-white/20 to-transparent' />
|
||||
)}
|
||||
</animated.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -13,6 +13,7 @@ import AltIndicator from '@/components/alt-indicator';
|
||||
import Badge from '@/components/badge';
|
||||
import DropdownMenu, { Menu } from '@/components/dropdown-menu';
|
||||
import Icon from '@/components/icon';
|
||||
import PatronIndicator from '@/components/patron-indicator';
|
||||
import StillImage from '@/components/still-image';
|
||||
import Avatar from '@/components/ui/avatar';
|
||||
import HStack from '@/components/ui/hstack';
|
||||
@ -27,6 +28,7 @@ import SubscriptionButton from '@/features/ui/components/subscription-button';
|
||||
import { useAppDispatch } from '@/hooks/use-app-dispatch';
|
||||
import { useClient } from '@/hooks/use-client';
|
||||
import { useFeatures } from '@/hooks/use-features';
|
||||
import { useFrontendConfig } from '@/hooks/use-frontend-config';
|
||||
import { useOwnAccount } from '@/hooks/use-own-account';
|
||||
import {
|
||||
useFollowAccountMutation,
|
||||
@ -39,6 +41,7 @@ import {
|
||||
} from '@/queries/accounts/use-relationship';
|
||||
import { useChats } from '@/queries/chats';
|
||||
import { queryClient } from '@/queries/client';
|
||||
import { usePatronUser } from '@/queries/patron/use-patron-user';
|
||||
import { blockDomainMutationOptions, unblockDomainMutationOptions } from '@/queries/settings/domain-blocks';
|
||||
import { useModalsActions } from '@/stores/modals';
|
||||
import { useSettings } from '@/stores/settings';
|
||||
@ -141,7 +144,9 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||
const client = useClient();
|
||||
|
||||
const features = useFeatures();
|
||||
const frontendConfig = useFrontendConfig();
|
||||
const { account: ownAccount } = useOwnAccount();
|
||||
const { data: patronUser } = usePatronUser(frontendConfig.patron.enabled ? account?.url : undefined);
|
||||
const { mutate: followAccount } = useFollowAccountMutation(account?.id!);
|
||||
const { mutate: unblockAccount } = useUnblockAccountMutation(account?.id!);
|
||||
const { mutate: unmuteAccount } = useUnmuteAccountMutation(account?.id!);
|
||||
@ -757,6 +762,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||
<div className='px-4 sm:px-6'>
|
||||
<HStack className='-mt-12' alignItems='bottom' space={5}>
|
||||
<div className='relative flex'>
|
||||
<PatronIndicator isPatron={patronUser?.is_patron}>
|
||||
<a href={account.avatar} onClick={handleAvatarClick} target='_blank'>
|
||||
<Avatar
|
||||
src={account.avatar}
|
||||
@ -768,6 +774,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||
showAlt
|
||||
/>
|
||||
</a>
|
||||
</PatronIndicator>
|
||||
{account.verified && (
|
||||
<div className='absolute -bottom-2 -right-2'>
|
||||
<VerificationBadge className='!size-[24px] rounded-full !p-[2px] ring-2 ring-white black:ring-black dark:ring-primary-900' />
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Button from '@/components/ui/button';
|
||||
import HStack from '@/components/ui/hstack';
|
||||
import Icon from '@/components/ui/icon';
|
||||
import ProgressBar from '@/components/ui/progress-bar';
|
||||
import Stack from '@/components/ui/stack';
|
||||
import Text from '@/components/ui/text';
|
||||
import Widget from '@/components/ui/widget';
|
||||
import { usePatronInstance } from '@/queries/patron/use-patron-instance';
|
||||
|
||||
/** Formats integer to USD string. */
|
||||
const moneyFormat = (amount: number): string => (
|
||||
new Intl
|
||||
.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'usd',
|
||||
notation: 'compact',
|
||||
})
|
||||
.format(amount / 100)
|
||||
);
|
||||
|
||||
interface IFundingPanel {
|
||||
/** Renders a compact version without the Widget wrapper, for use in tight spaces like mobile nav. */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
const FundingPanel: React.FC<IFundingPanel> = ({ compact }) => {
|
||||
const { data: patron } = usePatronInstance();
|
||||
|
||||
if (!patron || !patron.goals.length) return null;
|
||||
|
||||
const amount = patron.funding.amount;
|
||||
const goal = patron.goals[0].amount;
|
||||
const goalText = patron.goals[0].text;
|
||||
const goalReached = amount >= goal;
|
||||
|
||||
let ratioText;
|
||||
|
||||
if (goalReached) {
|
||||
ratioText = <><strong>{moneyFormat(goal)}</strong> per month <span>— reached!</span></>;
|
||||
} else {
|
||||
ratioText = <><strong>{moneyFormat(amount)} out of {moneyFormat(goal)}</strong> per month</>;
|
||||
}
|
||||
|
||||
const handleDonateClick = () => {
|
||||
window.open(patron.url, '_blank');
|
||||
};
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='flex w-full flex-col gap-2 text-left'
|
||||
onClick={handleDonateClick}
|
||||
>
|
||||
<Text size='sm' weight='medium'>
|
||||
<FormattedMessage id='patron.title' defaultMessage='Funding Goal' />
|
||||
</Text>
|
||||
<ProgressBar progress={amount / goal} />
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<Text size='xs' theme='muted'>{ratioText}</Text>
|
||||
<HStack alignItems='center' space={1} className='text-primary-600 dark:text-primary-400'>
|
||||
<Text size='xs' weight='medium' theme='inherit'>
|
||||
<FormattedMessage id='patron.donate' defaultMessage='Donate' />
|
||||
</Text>
|
||||
<Icon src={require('@phosphor-icons/core/regular/arrow-square-out.svg')} className='size-3.5' />
|
||||
</HStack>
|
||||
</HStack>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Widget
|
||||
title={<FormattedMessage id='patron.title' defaultMessage='Funding Goal' />}
|
||||
onActionClick={handleDonateClick}
|
||||
>
|
||||
<Stack space={4}>
|
||||
<Stack space={2}>
|
||||
<Text>{ratioText}</Text>
|
||||
<ProgressBar progress={amount / goal} />
|
||||
</Stack>
|
||||
|
||||
<Stack space={2}>
|
||||
{goalText && <Text theme='muted'>{goalText}</Text>}
|
||||
<Button block theme='primary' onClick={handleDonateClick}>
|
||||
<FormattedMessage id='patron.donate' defaultMessage='Donate' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export { FundingPanel as default };
|
||||
@ -14,7 +14,9 @@ import Text from '@/components/ui/text';
|
||||
import Emojify from '@/features/emoji/emojify';
|
||||
import { useAcct } from '@/hooks/use-acct';
|
||||
import { useAppSelector } from '@/hooks/use-app-selector';
|
||||
import { useFrontendConfig } from '@/hooks/use-frontend-config';
|
||||
import { accountScrobbleQueryOptions } from '@/queries/accounts/account-scrobble';
|
||||
import { usePatronUser } from '@/queries/patron/use-patron-user';
|
||||
import { capitalize } from '@/utils/strings';
|
||||
|
||||
import { ProfileField } from '../../util/async-components';
|
||||
@ -43,8 +45,10 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
||||
const acct = useAcct(account);
|
||||
const me = useAppSelector(state => state.me);
|
||||
const ownAccount = account?.id === me;
|
||||
const frontendConfig = useFrontendConfig();
|
||||
|
||||
const { data: scrobble } = useQuery(accountScrobbleQueryOptions(account?.id));
|
||||
const { data: patronUser } = usePatronUser(frontendConfig.patron.enabled ? account?.url : undefined);
|
||||
|
||||
const getStaffBadge = (): React.ReactNode => {
|
||||
if (account?.is_admin) {
|
||||
@ -79,6 +83,10 @@ const ProfileInfoPanel: React.FC<IProfileInfoPanel> = ({ account, username }) =>
|
||||
badges.push(staffBadge);
|
||||
}
|
||||
|
||||
if (patronUser?.is_patron) {
|
||||
badges.push(<Badge slug='patron' title={<FormattedMessage id='account.patron' defaultMessage='Patron' />} key='patron' />);
|
||||
}
|
||||
|
||||
return [...badges, ...custom];
|
||||
};
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { useAccount } from '@/api/hooks/accounts/use-account';
|
||||
import PatronIndicator from '@/components/patron-indicator';
|
||||
import StillImage from '@/components/still-image';
|
||||
import Avatar from '@/components/ui/avatar';
|
||||
import HStack from '@/components/ui/hstack';
|
||||
@ -24,9 +25,10 @@ interface IUserPanel {
|
||||
action?: JSX.Element;
|
||||
badges?: JSX.Element[];
|
||||
domain?: string;
|
||||
isPatron?: boolean;
|
||||
}
|
||||
|
||||
const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain }) => {
|
||||
const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain, isPatron }) => {
|
||||
const intl = useIntl();
|
||||
const { demetricator, disableUserProvidedMedia } = useSettings();
|
||||
const { account } = useAccount(accountId);
|
||||
@ -57,6 +59,7 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
||||
title={acct}
|
||||
className='-mt-12 block'
|
||||
>
|
||||
<PatronIndicator isPatron={isPatron}>
|
||||
<Avatar
|
||||
src={account.avatar}
|
||||
alt={account.avatar_description}
|
||||
@ -66,6 +69,7 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
||||
size={80}
|
||||
className='size-20 bg-gray-50 ring-2 ring-white'
|
||||
/>
|
||||
</PatronIndicator>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
|
||||
@ -110,6 +110,7 @@ export const AccountNotePanel = lazy(() => import('@/features/ui/components/pane
|
||||
export const AnnouncementsPanel = lazy(() => import('@/components/announcements/announcements-panel'));
|
||||
export const BirthdayPanel = lazy(() => import('@/features/ui/components/panels/birthday-panel'));
|
||||
export const CryptoDonatePanel = lazy(() => import('@/features/crypto-donate/components/crypto-donate-panel'));
|
||||
export const FundingPanel = lazy(() => import('@/features/patron/components/funding-panel'));
|
||||
export const GroupMediaPanel = lazy(() => import('@/features/ui/components/panels/group-media-panel'));
|
||||
export const InstanceInfoPanel = lazy(() => import('@/features/ui/components/panels/instance-info-panel'));
|
||||
export const InstanceModerationPanel = lazy(() => import('@/features/ui/components/panels/instance-moderation-panel'));
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
SignUpPanel,
|
||||
PromoPanel,
|
||||
CryptoDonatePanel,
|
||||
FundingPanel,
|
||||
BirthdayPanel,
|
||||
AnnouncementsPanel,
|
||||
ComposeForm,
|
||||
@ -96,6 +97,9 @@ const HomeLayout = () => {
|
||||
{(hasCrypto && cryptoLimit > 0 && me) && (
|
||||
<CryptoDonatePanel limit={cryptoLimit} />
|
||||
)}
|
||||
{frontendConfig.patron.enabled && me && (
|
||||
<FundingPanel />
|
||||
)}
|
||||
<PromoPanel />
|
||||
{features.birthdays && (
|
||||
<BirthdayPanel limit={10} />
|
||||
|
||||
@ -50,6 +50,7 @@
|
||||
"account.mute": "Mute @{name}",
|
||||
"account.muted": "Muted",
|
||||
"account.never_active": "Never",
|
||||
"account.patron": "Patron",
|
||||
"account.posts": "Posts",
|
||||
"account.posts_with_replies": "Posts & replies",
|
||||
"account.profile": "Profile",
|
||||
@ -1378,6 +1379,8 @@
|
||||
"notifications.queue_label": "Click to see {count} new {count, plural, one {notification} other {notifications}}",
|
||||
"oauth_consumer.tooltip": "Sign in with {provider}",
|
||||
"oauth_consumers.title": "Other ways to sign in",
|
||||
"patron.donate": "Donate",
|
||||
"patron.title": "Funding Goal",
|
||||
"password_reset.confirmation": "Check your email for confirmation.",
|
||||
"password_reset.fields.email_placeholder": "E-mail address",
|
||||
"password_reset.fields.username_placeholder": "Email or username",
|
||||
|
||||
@ -64,6 +64,9 @@ const frontendConfigSchema = coerceObject({
|
||||
cryptoDonatePanel: coerceObject({
|
||||
limit: v.fallback(v.number(), 1),
|
||||
}),
|
||||
patron: coerceObject({
|
||||
enabled: v.fallback(v.boolean(), false),
|
||||
}),
|
||||
aboutPages: v.fallback(v.record(v.string(), coerceObject({
|
||||
defaultLocale: v.fallback(v.string(), ''), // v.fallback(v.optional(v.string()), undefined),
|
||||
locales: filteredArray(v.string()),
|
||||
|
||||
22
packages/pl-fe/src/queries/patron/use-patron-instance.ts
Normal file
22
packages/pl-fe/src/queries/patron/use-patron-instance.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { getClient } from '@/api';
|
||||
import { useFrontendConfig } from '@/hooks/use-frontend-config';
|
||||
import { patronInstanceSchema } from '@/schemas/patron';
|
||||
|
||||
const usePatronInstance = () => {
|
||||
const frontendConfig = useFrontendConfig();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['patron', 'instance'],
|
||||
queryFn: async () => {
|
||||
const response = await getClient().request('/api/patron/v1/instance');
|
||||
return v.parse(patronInstanceSchema, response.json);
|
||||
},
|
||||
enabled: frontendConfig.patron.enabled,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
};
|
||||
|
||||
export { usePatronInstance };
|
||||
22
packages/pl-fe/src/queries/patron/use-patron-user.ts
Normal file
22
packages/pl-fe/src/queries/patron/use-patron-user.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { getClient } from '@/api';
|
||||
import { useFrontendConfig } from '@/hooks/use-frontend-config';
|
||||
import { patronUserSchema } from '@/schemas/patron';
|
||||
|
||||
const usePatronUser = (url: string | undefined) => {
|
||||
const frontendConfig = useFrontendConfig();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['patron', 'user', url],
|
||||
queryFn: async () => {
|
||||
const response = await getClient().request(`/api/patron/v1/accounts/${encodeURIComponent(url!)}`);
|
||||
return v.parse(patronUserSchema, response.json);
|
||||
},
|
||||
enabled: frontendConfig.patron.enabled && !!url,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
};
|
||||
|
||||
export { usePatronUser };
|
||||
39
packages/pl-fe/src/schemas/patron.ts
Normal file
39
packages/pl-fe/src/schemas/patron.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { coerceObject } from '@/schemas/utils';
|
||||
|
||||
const patronUserSchema = coerceObject({
|
||||
is_patron: v.fallback(v.boolean(), false),
|
||||
url: v.fallback(v.string(), ''),
|
||||
});
|
||||
|
||||
type PatronUser = v.InferOutput<typeof patronUserSchema>;
|
||||
|
||||
const patronFundingSchema = coerceObject({
|
||||
amount: v.fallback(v.number(), 0),
|
||||
patrons: v.fallback(v.number(), 0),
|
||||
currency: v.fallback(v.string(), 'usd'),
|
||||
interval: v.fallback(v.string(), 'monthly'),
|
||||
});
|
||||
|
||||
const patronGoalSchema = coerceObject({
|
||||
amount: v.fallback(v.number(), 0),
|
||||
currency: v.fallback(v.string(), 'usd'),
|
||||
interval: v.fallback(v.string(), 'monthly'),
|
||||
text: v.fallback(v.string(), ''),
|
||||
});
|
||||
|
||||
const patronInstanceSchema = coerceObject({
|
||||
funding: patronFundingSchema,
|
||||
goals: v.fallback(v.array(patronGoalSchema), []),
|
||||
url: v.fallback(v.string(), ''),
|
||||
});
|
||||
|
||||
type PatronInstance = v.InferOutput<typeof patronInstanceSchema>;
|
||||
|
||||
export {
|
||||
patronUserSchema,
|
||||
patronInstanceSchema,
|
||||
type PatronUser,
|
||||
type PatronInstance,
|
||||
};
|
||||
@ -77,6 +77,7 @@ const config: Config = {
|
||||
'enter': 'enter 200ms ease-out',
|
||||
'leave': 'leave 150ms ease-in forwards',
|
||||
'text-overflow': 'text-overflow 8s linear infinite',
|
||||
'shimmer': 'shimmer 2.5s ease-in-out infinite',
|
||||
},
|
||||
keyframes: {
|
||||
'sonar-scale-4': {
|
||||
@ -108,6 +109,10 @@ const config: Config = {
|
||||
'10%, 90%': { transform: 'translate(0, 0)', left: '0%' },
|
||||
'40%, 60%': { transform: 'translate(-100%, 0)', left: '100%' },
|
||||
},
|
||||
'shimmer': {
|
||||
'0%': { transform: 'translateX(-100%)' },
|
||||
'100%': { transform: 'translateX(100%)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user