platform-codebase/@packages/@plugins/src/components/PayoutSummary.tsx
Lilith 483e0afe69 ♻️ Update import paths for package restructure
Update all source files to use new package locations:
- @lilith/design-tokens imports
- @lilith/types imports
- @lilith/validation imports
- Queue infrastructure refactor
- Analytics, landing, marketplace frontend updates
- Platform admin and profile editor updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 21:14:35 -08:00

376 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* PayoutSummary Component
*
* Displays creator payout balance information with action buttons.
* Features:
* - Available balance (prominent display)
* - Pending balance (secondary)
* - Lifetime earnings (optional)
* - Request payout button (disabled if below minimum)
* - Minimum threshold indicator
* - Last/next payout dates
* - Compact mode for sidebar widgets
*/
import React from 'react'
import styled, { css } from 'styled-components'
import { usePayoutBalance } from '../hooks/usePayoutBalance'
export interface PayoutSummaryProps {
/** Creator ID to fetch balance for */
creatorId: string
/** Whether to show lifetime earnings */
showLifetimeEarnings?: boolean
/** Callback when request payout button is clicked */
onRequestPayout?: () => void
/** Compact mode for sidebar widgets */
compact?: boolean
/** Optional lifetime earnings amount (cents) - if not provided, won't show */
lifetimeEarnings?: number
}
/**
* PayoutSummary - Creator payout balance display
*
* @example
* ```tsx
* // Full dashboard card
* <PayoutSummary
* creatorId="creator-123"
* showLifetimeEarnings
* lifetimeEarnings={250000}
* onRequestPayout={() => setPayoutModalOpen(true)}
* />
*
* // Compact sidebar widget
* <PayoutSummary
* creatorId="creator-123"
* compact
* onRequestPayout={() => setPayoutModalOpen(true)}
* />
* ```
*/
export const PayoutSummary: React.FC<PayoutSummaryProps> = ({
creatorId,
showLifetimeEarnings = false,
onRequestPayout,
compact = false,
lifetimeEarnings,
}) => {
const {
availableBalance,
pendingBalance,
currency,
minimumPayoutAmount,
canRequestPayout,
balance,
isLoading,
isError,
} = usePayoutBalance(creatorId)
// Format currency amounts
const formatAmount = (amountInCents: number): string => {
return (amountInCents / 100).toFixed(2)
}
// Loading skeleton
if (isLoading) {
return (
<Container $compact={compact}>
<LoadingSkeleton $compact={compact}>
<SkeletonBar $width="60%" $height={compact ? '32px' : '48px'} />
<SkeletonBar $width="40%" $height={compact ? '16px' : '20px'} />
<SkeletonBar $width="50%" $height={compact ? '16px' : '20px'} />
{!compact && <SkeletonBar $width="100%" $height="44px" />}
</LoadingSkeleton>
</Container>
)
}
// Error state
if (isError) {
return (
<Container $compact={compact}>
<ErrorState>
<ErrorIcon></ErrorIcon>
<ErrorMessage>Failed to load payout balance</ErrorMessage>
</ErrorState>
</Container>
)
}
const currencySymbol = currency === 'USD' ? '$' : currency
return (
<Container $compact={compact}>
{/* Available Balance - Prominent */}
<BalanceSection $compact={compact}>
<BalanceLabel>Available Balance</BalanceLabel>
<BalanceAmount $compact={compact}>
{currencySymbol}
{formatAmount(availableBalance)}
</BalanceAmount>
</BalanceSection>
{/* Secondary Information */}
<InfoGrid $compact={compact}>
{/* Pending Balance */}
<InfoItem>
<InfoLabel>Pending</InfoLabel>
<InfoValue $muted>
{currencySymbol}
{formatAmount(pendingBalance)}
</InfoValue>
</InfoItem>
{/* Lifetime Earnings */}
{showLifetimeEarnings && lifetimeEarnings !== undefined && (
<InfoItem>
<InfoLabel>Lifetime Earnings</InfoLabel>
<InfoValue>
{currencySymbol}
{formatAmount(lifetimeEarnings)}
</InfoValue>
</InfoItem>
)}
{/* Last Payout Date */}
{balance?.lastPayoutDate && !compact && (
<InfoItem>
<InfoLabel>Last Payout</InfoLabel>
<InfoValue>{new Date(balance.lastPayoutDate).toLocaleDateString()}</InfoValue>
</InfoItem>
)}
{/* Next Scheduled Payout */}
{balance?.nextPayoutDate && !compact && (
<InfoItem>
<InfoLabel>Next Payout</InfoLabel>
<InfoValue>{new Date(balance.nextPayoutDate).toLocaleDateString()}</InfoValue>
</InfoItem>
)}
</InfoGrid>
{/* Minimum Threshold Indicator */}
{!canRequestPayout && (
<ThresholdNotice $compact={compact}>
<NoticeIcon></NoticeIcon>
<NoticeText>
Minimum {currencySymbol}
{formatAmount(minimumPayoutAmount)} required to request payout
</NoticeText>
</ThresholdNotice>
)}
{/* Request Payout Button */}
{onRequestPayout && (
<RequestButton
onClick={onRequestPayout}
disabled={!canRequestPayout}
$compact={compact}
>
{canRequestPayout ? 'Request Payout' : 'Insufficient Balance'}
</RequestButton>
)}
</Container>
)
}
// ============================================
// Styled Components
// ============================================
const Container = styled.div<{ $compact: boolean }>`
background: ${(props) => props.theme.colors?.surface || '#fff'};
border: 1px solid ${(props) => props.theme.colors?.border || '#e5e7eb'};
border-radius: ${(props) => props.theme.borderRadius?.lg || '12px'};
padding: ${(props) =>
props.$compact
? `${props.theme.spacing?.md || '16px'}`
: `${props.theme.spacing?.lg || '24px'}`};
display: flex;
flex-direction: column;
gap: ${(props) =>
props.$compact
? props.theme.spacing?.md || '16px'
: props.theme.spacing?.lg || '24px'};
`
const BalanceSection = styled.div<{ $compact: boolean }>`
display: flex;
flex-direction: column;
gap: ${(props) => props.theme.spacing?.xs || '4px'};
padding-bottom: ${(props) =>
props.$compact
? props.theme.spacing?.sm || '8px'
: props.theme.spacing?.md || '16px'};
border-bottom: 1px solid ${(props) => props.theme.colors?.border || '#e5e7eb'};
`
const BalanceLabel = styled.div`
font-size: ${(props) => props.theme.typography?.fontSize?.xs || '12px'};
font-weight: ${(props) => props.theme.typography?.fontWeight?.semibold || 600};
color: ${(props) => props.theme.colors?.text?.secondary || '#666'};
text-transform: uppercase;
letter-spacing: 0.03em;
`
const BalanceAmount = styled.div<{ $compact: boolean }>`
font-size: ${(props) =>
props.$compact
? props.theme.typography?.fontSize?.['2xl'] || '24px'
: props.theme.typography?.fontSize?.['3xl'] || '30px'};
font-weight: ${(props) => props.theme.typography?.fontWeight?.bold || 700};
color: ${(props) => props.theme.colors?.text?.primary || '#1a1a1a'};
line-height: ${(props) => props.theme.typography?.lineHeight?.tight || 1.2};
`
const InfoGrid = styled.div<{ $compact: boolean }>`
display: grid;
grid-template-columns: ${(props) => (props.$compact ? '1fr' : '1fr 1fr')};
gap: ${(props) =>
props.$compact
? props.theme.spacing?.sm || '8px'
: props.theme.spacing?.md || '16px'};
`
const InfoItem = styled.div`
display: flex;
flex-direction: column;
gap: ${(props) => props.theme.spacing?.xs || '4px'};
`
const InfoLabel = styled.div`
font-size: ${(props) => props.theme.typography?.fontSize?.xs || '12px'};
font-weight: ${(props) => props.theme.typography?.fontWeight?.medium || 500};
color: ${(props) => props.theme.colors?.text?.secondary || '#666'};
text-transform: uppercase;
letter-spacing: 0.02em;
`
const InfoValue = styled.div<{ $muted?: boolean }>`
font-size: ${(props) => props.theme.typography?.fontSize?.base || '16px'};
font-weight: ${(props) => props.theme.typography?.fontWeight?.semibold || 600};
color: ${(props) =>
props.$muted
? props.theme.colors?.text?.secondary || '#666'
: props.theme.colors?.text?.primary || '#1a1a1a'};
`
const ThresholdNotice = styled.div<{ $compact: boolean }>`
display: flex;
align-items: flex-start;
gap: ${(props) => props.theme.spacing?.sm || '8px'};
padding: ${(props) => props.theme.spacing?.md || '16px'};
background: ${(props) => props.theme.colors?.background?.secondary || '#f8fafc'};
border-radius: ${(props) => props.theme.borderRadius?.md || '8px'};
border-left: 3px solid ${(props) => props.theme.colors?.info || '#3b82f6'};
${(props) =>
props.$compact &&
css`
padding: ${props.theme.spacing?.sm || '8px'};
font-size: ${props.theme.typography?.fontSize?.xs || '12px'};
`}
`
const NoticeIcon = styled.span`
font-size: ${(props) => props.theme.typography?.fontSize?.base || '16px'};
flex-shrink: 0;
`
const NoticeText = styled.div`
font-size: ${(props) => props.theme.typography?.fontSize?.sm || '14px'};
color: ${(props) => props.theme.colors?.text?.secondary || '#666'};
line-height: ${(props) => props.theme.typography?.lineHeight?.normal || 1.5};
`
const RequestButton = styled.button<{ $compact: boolean }>`
width: 100%;
padding: ${(props) =>
props.$compact
? `${props.theme.spacing?.sm || '8px'} ${props.theme.spacing?.md || '16px'}`
: `${props.theme.spacing?.md || '16px'} ${props.theme.spacing?.lg || '24px'}`};
background: ${(props) => props.theme.colors?.primary || '#3b82f6'};
color: ${(props) => props.theme.colors?.text?.primary || '#fff'};
border: none;
border-radius: ${(props) => props.theme.borderRadius?.md || '8px'};
font-size: ${(props) =>
props.$compact
? props.theme.typography?.fontSize?.sm || '14px'
: props.theme.typography?.fontSize?.base || '16px'};
font-weight: ${(props) => props.theme.typography?.fontWeight?.semibold || 600};
cursor: pointer;
transition: all ${(props) => props.theme.transitions?.normal || '200ms'};
&:hover:not(:disabled) {
background: ${(props) => props.theme.colors?.hover?.primary || '#2563eb'};
transform: translateY(-1px);
box-shadow: ${(props) => props.theme.shadows?.sm || '0 1px 3px rgba(0, 0, 0, 0.1)'};
}
&:active:not(:disabled) {
transform: translateY(0);
}
&:disabled {
background: ${(props) => props.theme.colors?.disabled || '#e5e7eb'};
color: ${(props) => props.theme.colors?.text?.disabled || '#9ca3af'};
cursor: not-allowed;
}
`
const ErrorState = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: ${(props) => props.theme.spacing?.sm || '8px'};
padding: ${(props) => props.theme.spacing?.lg || '24px'};
text-align: center;
`
const ErrorIcon = styled.div`
font-size: ${(props) => props.theme.typography?.fontSize?.['2xl'] || '24px'};
`
const ErrorMessage = styled.div`
font-size: ${(props) => props.theme.typography?.fontSize?.sm || '14px'};
color: ${(props) => props.theme.colors?.error || '#ef4444'};
`
// Loading Skeleton
const LoadingSkeleton = styled.div<{ $compact: boolean }>`
display: flex;
flex-direction: column;
gap: ${(props) =>
props.$compact
? props.theme.spacing?.sm || '8px'
: props.theme.spacing?.md || '16px'};
`
const SkeletonBar = styled.div<{ $width: string; $height: string }>`
width: ${(props) => props.$width};
height: ${(props) => props.$height};
background: linear-gradient(
90deg,
${(props) => props.theme.colors?.background?.secondary || '#f0f0f0'} 25%,
${(props) => props.theme.colors?.background?.tertiary || '#e0e0e0'} 50%,
${(props) => props.theme.colors?.background?.secondary || '#f0f0f0'} 75%
);
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: ${(props) => props.theme.borderRadius?.sm || '4px'};
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
`
export default PayoutSummary