feat(chat): add support action cards and live entitlements
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ArrowRight,
|
||||
CircleDollarSign,
|
||||
CircleHelp,
|
||||
Gift,
|
||||
Images,
|
||||
LoaderCircle,
|
||||
MessageSquareWarning,
|
||||
UserRound,
|
||||
WalletCards,
|
||||
X,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import type { ChatAction } from "@/data/schemas/chat";
|
||||
|
||||
import styles from "./chat-area.module.css";
|
||||
|
||||
const ACTION_ICONS: Record<ChatAction["type"], LucideIcon> = {
|
||||
giftOffer: Gift,
|
||||
privateAlbumOffer: Images,
|
||||
openProfile: UserRound,
|
||||
openWallet: WalletCards,
|
||||
openCoinsRules: WalletCards,
|
||||
activateVip: CircleDollarSign,
|
||||
topUp: CircleDollarSign,
|
||||
openFeedback: MessageSquareWarning,
|
||||
resumePayment: CircleHelp,
|
||||
};
|
||||
|
||||
export interface ChatActionCardProps {
|
||||
action: ChatAction;
|
||||
onViewed?: (action: ChatAction) => void;
|
||||
onActivate?: (action: ChatAction) => void | Promise<void>;
|
||||
onDismiss?: (action: ChatAction) => void;
|
||||
}
|
||||
|
||||
export function ChatActionCard({
|
||||
action,
|
||||
onViewed,
|
||||
onActivate,
|
||||
onDismiss,
|
||||
}: ChatActionCardProps) {
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [activating, setActivating] = useState(false);
|
||||
const [activationError, setActivationError] = useState<string | null>(null);
|
||||
const viewedActionIdRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (viewedActionIdRef.current === action.actionId) return;
|
||||
viewedActionIdRef.current = action.actionId;
|
||||
onViewed?.(action);
|
||||
}, [action, onViewed]);
|
||||
|
||||
if (dismissed) return null;
|
||||
const Icon = ACTION_ICONS[action.type];
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={styles.commercialAction}
|
||||
aria-label={action.ctaLabel}
|
||||
data-chat-action={action.type}
|
||||
data-chat-action-kind={action.kind}
|
||||
>
|
||||
<div className={styles.commercialActionHeader}>
|
||||
<Icon size={18} aria-hidden="true" />
|
||||
<button
|
||||
type="button"
|
||||
className={styles.commercialActionDismiss}
|
||||
title="Dismiss"
|
||||
aria-label="Dismiss"
|
||||
onClick={() => {
|
||||
setDismissed(true);
|
||||
onDismiss?.(action);
|
||||
}}
|
||||
>
|
||||
<X size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<p className={styles.commercialActionCopy}>{action.copy}</p>
|
||||
{activationError ? (
|
||||
<p className={styles.commercialActionError} role="alert">
|
||||
{activationError}
|
||||
</p>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.commercialActionButton}
|
||||
disabled={activating}
|
||||
onClick={() => {
|
||||
setActivating(true);
|
||||
setActivationError(null);
|
||||
void Promise.resolve(onActivate?.(action))
|
||||
.catch(() =>
|
||||
setActivationError(
|
||||
"This option is unavailable right now. Please try again.",
|
||||
),
|
||||
)
|
||||
.finally(() => setActivating(false));
|
||||
}}
|
||||
>
|
||||
<span>{action.ctaLabel}</span>
|
||||
{activating ? (
|
||||
<LoaderCircle
|
||||
className={styles.commercialActionSpinner}
|
||||
size={17}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<ArrowRight size={17} aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user