Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/components/common/CreatorBreadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { Link } from 'react-router';
import { ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { normalizeCreatorDisplayName } from '@/utils/creatorDisplayName.utils';

interface CreatorBreadcrumbProps {
parentLabel: string;
Expand All @@ -16,6 +17,9 @@ const CreatorBreadcrumb: React.FC<CreatorBreadcrumbProps> = ({
currentLabel,
className,
}) => {
const displayCurrentLabel =
normalizeCreatorDisplayName(currentLabel) || 'Unnamed creator';

return (
<nav
aria-label="Breadcrumb"
Expand All @@ -32,7 +36,7 @@ const CreatorBreadcrumb: React.FC<CreatorBreadcrumbProps> = ({
</Link>
<ChevronRight className="size-4 text-white/30" />
<span className="font-bold text-white truncate max-w-[150px] sm:max-w-none">
{currentLabel}
{displayCurrentLabel}
</span>
</nav>
);
Expand Down
116 changes: 92 additions & 24 deletions src/components/common/CreatorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import { useRef, useState } from 'react';
import { useAccount } from 'wagmi';
import type { Course } from '@/services/course.service';
import { cn } from '@/lib/utils';
import { ShoppingCart, Link as LinkIcon, TrendingUp, MoreVertical, Copy, Share2, ExternalLink } from 'lucide-react';
import {
ShoppingCart,
Link as LinkIcon,
TrendingUp,
MoreVertical,
Copy,
Share2,
ExternalLink,
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
Expand All @@ -17,6 +25,8 @@ import showToast from '@/utils/toast.util';
import { formatCompactNumber } from '@/utils/numberFormat.utils';
import { formatCreatorKeyPriceDisplay } from '@/utils/keyPriceDisplay.utils';
import { formatCreatorHandle } from '@/utils/handleDisplay.utils';
import { normalizeCreatorDisplayName } from '@/utils/creatorDisplayName.utils';
import { getCreatorPriceChartAccessibilityCopy } from '@/utils/creatorPriceChartAccessibility.utils';
import { formatJoinDate } from '@/utils/formatJoinDate';
import { useSystemTheme } from '@/utils/useSystemTheme';
import { Tooltip } from '@/components/ui/tooltip';
Expand Down Expand Up @@ -68,22 +78,36 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
const displayInstructorHandle =
formatCreatorHandle(creator.instructorId) || '@creator';
const displaySocialHandle = formatCreatorHandle(creator.socialHandle);
const displayCreatorName =
normalizeCreatorDisplayName(creator.title) || 'Unnamed creator';
const priceChartAccessibility = getCreatorPriceChartAccessibilityCopy({
name: displayCreatorName,
price: creator.price,
priceStroops: creator.priceStroops,
change24h: creator.change24h,
});
const priceChartDescriptionId = `creator-price-chart-description-${creator.id}`;
const { isConnected } = useAccount();
const { isMismatch: isNetworkMismatch, expectedChainName } = useNetworkMismatch();
const { isMismatch: isNetworkMismatch, expectedChainName } =
useNetworkMismatch();
const [transactionState, setTransactionState] = useState<
'idle' | 'submitting' | 'failed' | 'success'
>('idle');
const [failureDrawerOpen, setFailureDrawerOpen] = useState(false);
const [failureDetails, setFailureDetails] = useState<TransactionFailureDetails>({
errorMessage: '',
});
const [failureDetails, setFailureDetails] =
useState<TransactionFailureDetails>({
errorMessage: '',
});
const hasFailedOnceRef = useRef(false);
const trackTransactionEvent = useTransactionTelemetry();

const runPurchaseAttempt = () => {
setTransactionState('submitting');
trackTransactionEvent('tx_submitted', { creatorId: creator.id, creatorTitle: creator.title });
showToast.loading(`Purchasing keys for ${creator.title}...`);
trackTransactionEvent('tx_submitted', {
creatorId: creator.id,
creatorTitle: displayCreatorName,
});
showToast.loading(`Purchasing keys for ${displayCreatorName}...`);

window.setTimeout(() => {
toast.remove();
Expand All @@ -92,7 +116,8 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
hasFailedOnceRef.current = true;
setTransactionState('failed');
setFailureDetails({
errorMessage: 'Transaction failed: Insufficient balance to complete the purchase.',
errorMessage:
'Transaction failed: Insufficient balance to complete the purchase.',
errorCode: 'ERR_INSUFFICIENT_BALANCE',
txHash: '0xabcd1234...failed',
developerDetails: {
Expand All @@ -108,10 +133,13 @@ const CreatorCard: React.FC<CreatorCardProps> = ({

hasFailedOnceRef.current = false;
setTransactionState('success');
trackTransactionEvent('tx_confirmed', { creatorId: creator.id, creatorTitle: creator.title });
trackTransactionEvent('tx_confirmed', {
creatorId: creator.id,
creatorTitle: displayCreatorName,
});
showToast.transactionSuccess(
'Purchase Successful!',
`You successfully bought a key for ${creator.title}`,
`You successfully bought a key for ${displayCreatorName}`,
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
'https://stellar.expert/explorer/testnet/tx/0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
);
Expand All @@ -136,7 +164,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
const handleShare = () => {
const url = `${window.location.origin}/creator/${creator.id}`;
if (navigator.share) {
navigator.share({ title: creator.title, url }).catch(() => {});
navigator.share({ title: displayCreatorName, url }).catch(() => {});
} else {
navigator.clipboard
.writeText(url)
Expand All @@ -160,7 +188,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
return;
}

toast.success(`Purchasing keys for ${creator.title}...`, {
toast.success(`Purchasing keys for ${displayCreatorName}...`, {
duration: 3000,
});
// Implementation for contract interaction would go here
Expand All @@ -177,7 +205,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
<div className="absolute right-3 top-3 z-20">
<DropdownMenu>
<DropdownMenuTrigger
aria-label={`More actions for ${creator.title}`}
aria-label={`More actions for ${displayCreatorName}`}
className="flex size-8 items-center justify-center rounded-full text-white/40 transition-colors hover:bg-white/10 hover:text-white/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-400/60"
>
<MoreVertical className="size-4" aria-hidden="true" />
Expand All @@ -187,7 +215,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
className="w-48 border-white/10 bg-slate-900/95 backdrop-blur-xl"
>
<DropdownMenuLabel className="text-xs text-white/50">
{creator.title}
{displayCreatorName}
</DropdownMenuLabel>
<DropdownMenuSeparator className="bg-white/10" />
<DropdownMenuItem
Expand Down Expand Up @@ -223,10 +251,13 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
aria-labelledby={`creator-name-${creator.id}`}
>
<CreatorInitialsAvatar
name={creator.title}
name={displayCreatorName}
creatorId={creator.id}
imageSrc={creator.thumbnail}
imageClassName={cn("transition-transform duration-500 motion-reduce:transition-none motion-safe:md:group-hover:scale-[1.03] motion-reduce:md:group-hover:scale-100", isDarkMode ? 'bg-gray-800' : 'bg-gray-100')}
imageClassName={cn(
'transition-transform duration-500 motion-reduce:transition-none motion-safe:md:group-hover:scale-[1.03] motion-reduce:md:group-hover:scale-100',
isDarkMode ? 'bg-gray-800' : 'bg-gray-100'
)}
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-transparent to-transparent opacity-0 transition-opacity duration-300 motion-reduce:transition-none motion-safe:md:group-hover:opacity-100 motion-reduce:md:group-hover:opacity-100" />
{creator.volume24h !== undefined && (
Expand All @@ -251,7 +282,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
id={`creator-name-${creator.id}`}
className="font-jakarta text-lg font-bold text-white"
>
{creator.title}
{displayCreatorName}
</h3>
<VerifiedBadge
verified={Boolean(creator.isVerified)}
Expand All @@ -273,7 +304,11 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
</CreatorHandleHoverCard>
</p>

<CreatorBio bio={creator.description} variant="card" className="mt-2" />
<CreatorBio
bio={creator.description}
variant="card"
className="mt-2"
/>

{creator.nextDropAt ? (
<CreatorDropCountdown nextDropAt={creator.nextDropAt} />
Expand Down Expand Up @@ -306,7 +341,29 @@ const CreatorCard: React.FC<CreatorCardProps> = ({

{/* Sparkline placeholder */}
<div className="mt-3">
<div className="h-10 w-full rounded-lg bg-white/10 animate-pulse" />
<div
role="img"
aria-label={priceChartAccessibility.summary}
aria-describedby={priceChartDescriptionId}
className="h-10 w-full rounded-lg bg-white/10 animate-pulse"
/>
<table id={priceChartDescriptionId} className="sr-only">
<caption>{priceChartAccessibility.summary}</caption>
<thead>
<tr>
<th scope="col">Point</th>
<th scope="col">Key price</th>
</tr>
</thead>
<tbody>
{priceChartAccessibility.points.map(point => (
<tr key={point.label}>
<th scope="row">{point.label}</th>
<td>{point.value}</td>
</tr>
))}
</tbody>
</table>
</div>

<div className="mt-3 flex flex-wrap gap-2">
Expand All @@ -323,7 +380,20 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
<CardMetaRow
label="Join Date"
value={
<Tooltip content={<p>{new Date(creator.joinedAt).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>}>
<Tooltip
content={
<p>
{new Date(creator.joinedAt).toLocaleDateString(
'en-US',
{
year: 'numeric',
month: 'long',
day: 'numeric',
}
)}
</p>
}
>
<span className="text-white/75 cursor-default truncate">
{formatJoinDate(creator.joinedAt)}
</span>
Expand All @@ -344,9 +414,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
: 'No public handle'
}
valueTitle={
creator.socialHandle
? displaySocialHandle
: undefined
creator.socialHandle ? displaySocialHandle : undefined
}
valueClassName={
creator.socialHandle
Expand Down Expand Up @@ -406,7 +474,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
id={`creator-card-actions-label-${creator.id}`}
className="sr-only"
>
Purchase actions for {creator.title}
Purchase actions for {displayCreatorName}
</span>
<NetworkFeeHint className="shrink-0" />
<AsyncButton
Expand Down
44 changes: 30 additions & 14 deletions src/components/common/CreatorProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import VerifiedBadge from '@/components/common/VerifiedBadge';
import CreatorInitialsAvatar from '@/components/common/CreatorInitialsAvatar';
import CreatorBio from '@/components/common/CreatorBio';
import { formatCreatorHandle } from '@/utils/handleDisplay.utils';
import { normalizeCreatorDisplayName } from '@/utils/creatorDisplayName.utils';
import { CREATOR_CARD_MEDIA_RADIUS_CLASS } from '@/utils/creatorCardTokens';

interface CreatorProfileHeaderProps {
Expand Down Expand Up @@ -47,6 +48,7 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
// Display-normalised handle; raw `handle` is preserved for any equality /
// URL construction the caller might do via the prop.
const displayHandle = formatCreatorHandle(handle);
const displayName = normalizeCreatorDisplayName(name) || 'Unnamed creator';

const handleShare = async () => {
let url = window.location.href;
Expand All @@ -57,7 +59,7 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
if (navigator.share) {
try {
await navigator.share({
title: `${name} (${displayHandle || `@${handle}`}) on Access Layer`,
title: `${displayName} (${displayHandle || `@${handle}`}) on Access Layer`,
url,
});
} catch (err) {
Expand Down Expand Up @@ -105,19 +107,27 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
CREATOR_CARD_MEDIA_RADIUS_CLASS
)}
>
<CreatorInitialsAvatar name={name} creatorId={creatorId} imageSrc={avatarUrl} />
<CreatorInitialsAvatar
name={displayName}
creatorId={creatorId}
imageSrc={avatarUrl}
/>
</motion.div>
<div className="min-w-0 space-y-0.5">
<div className="flex items-center gap-2 overflow-hidden">
<motion.h1
id="creator-profile-name"
animate={{ fontSize: isScrolled ? '1.25rem' : '1.875rem' }}
animate={{
fontSize: isScrolled ? '1.25rem' : '1.875rem',
}}
className={cn(
"truncate font-grotesque font-black tracking-tight text-white transition-all duration-300",
isScrolled ? "text-xl md:text-2xl" : "text-3xl md:text-4xl"
'truncate font-grotesque font-black tracking-tight text-white transition-all duration-300',
isScrolled
? 'text-xl md:text-2xl'
: 'text-3xl md:text-4xl'
)}
>
{name}
{displayName}
</motion.h1>
{isVerified && (
<div className="shrink-0">
Expand Down Expand Up @@ -150,16 +160,18 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
</div>
</div>

<div className={cn(
"flex items-center gap-3 transition-transform duration-300",
isScrolled ? "scale-90" : "scale-100"
)}>
<div
className={cn(
'flex items-center gap-3 transition-transform duration-300',
isScrolled ? 'scale-90' : 'scale-100'
)}
>
<Button
onClick={handleShare}
variant="outline"
className={cn(
"rounded-xl border-white/10 bg-white/5 font-bold text-white transition-all hover:border-amber-500/30 hover:bg-amber-500/10 active:scale-95",
isScrolled ? "h-9 px-3 text-xs" : "h-11 px-4 text-sm"
'rounded-xl border-white/10 bg-white/5 font-bold text-white transition-all hover:border-amber-500/30 hover:bg-amber-500/10 active:scale-95',
isScrolled ? 'h-9 px-3 text-xs' : 'h-11 px-4 text-sm'
)}
>
{copied ? (
Expand All @@ -170,7 +182,11 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
<Copy className="mr-2 size-4 text-amber-500" />
)}
<span className="hidden sm:inline">
{copied ? 'Copied!' : canNativeShare ? 'Share Profile' : 'Copy Profile Link'}
{copied
? 'Copied!'
: canNativeShare
? 'Share Profile'
: 'Copy Profile Link'}
</span>
<span className="sm:hidden">
{copied ? 'Copied' : canNativeShare ? 'Share' : 'Copy'}
Expand All @@ -182,4 +198,4 @@ const CreatorProfileHeader: React.FC<CreatorProfileHeaderProps> = ({
);
};

export default CreatorProfileHeader;
export default CreatorProfileHeader;
7 changes: 5 additions & 2 deletions src/components/common/TradeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import PercentageBadge from '@/components/common/PercentageBadge';
import NetworkFeeHint from '@/components/common/NetworkFeeHint';
import { TRADE_FEE_ESTIMATE } from '@/constants/fees';
import { formatTransactionFeeDisplay } from '@/utils/transactionFee.utils';
import { normalizeCreatorDisplayName } from '@/utils/creatorDisplayName.utils';

export type TradeSide = 'buy' | 'sell';

Expand Down Expand Up @@ -59,6 +60,8 @@ const TradeDialog: React.FC<TradeDialogProps> = ({
parsedAmount > 0 &&
(side !== 'sell' || parsedAmount <= availableHoldings);

const displayCreatorName =
normalizeCreatorDisplayName(creatorName) || 'Unnamed creator';
const title = side === 'buy' ? 'Buy keys' : 'Sell keys';
const confirmLabel = side === 'buy' ? 'Confirm buy' : 'Confirm sell';
const estimatedNetworkFee = formatTransactionFeeDisplay(
Expand Down Expand Up @@ -90,8 +93,8 @@ const TradeDialog: React.FC<TradeDialogProps> = ({
<DialogTitle>{title}</DialogTitle>
<DialogDescription>
{side === 'buy'
? `Purchase creator keys for ${creatorName}.`
: `Sell creator keys for ${creatorName}.`}
? `Purchase creator keys for ${displayCreatorName}.`
: `Sell creator keys for ${displayCreatorName}.`}
</DialogDescription>
</DialogHeader>

Expand Down
Loading
Loading