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: 3 additions & 3 deletions src/components/common/CreatorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
return (
<div
className={cn(
'marketplace-card-surface marketplace-card-surface-hover group relative overflow-hidden rounded-2xl border p-4 transition-all duration-300 focus-within:ring-2 focus-within:ring-amber-400/40 focus-within:ring-offset-2 focus-within:ring-offset-slate-950 md:hover:-translate-y-0.5 md:hover:border-amber-500/25 md:hover:shadow-[0_12px_32px_-20px_rgba(251,191,36,0.5)]',
'marketplace-card-surface marketplace-card-surface-hover group relative overflow-hidden rounded-2xl border p-4 transition-all duration-300 focus-within:ring-2 focus-within:ring-amber-400/40 focus-within:ring-offset-2 focus-within:ring-offset-slate-950 motion-reduce:transition-none motion-safe:md:hover:-translate-y-0.5 motion-safe:md:hover:border-amber-500/25 motion-safe:md:hover:shadow-[0_12px_32px_-20px_rgba(251,191,36,0.5)] motion-reduce:md:hover:translate-y-0 motion-reduce:md:hover:border-amber-500/35 motion-reduce:md:hover:bg-white/[0.05] motion-reduce:md:hover:shadow-[0_0_0_1px_rgba(251,191,36,0.12)]',
className
)}
>
Expand Down Expand Up @@ -218,9 +218,9 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
name={creator.title}
creatorId={creator.id}
imageSrc={creator.thumbnail}
imageClassName="transition-transform duration-500 md:group-hover:scale-[1.03]"
imageClassName="transition-transform duration-500 motion-reduce:transition-none motion-safe:md:group-hover:scale-[1.03] motion-reduce:md:group-hover:scale-100"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-transparent to-transparent opacity-0 transition-opacity duration-300 md:group-hover:opacity-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 && (
// #313: the .creator-card-overlay-text class swaps this
// pill to system high-contrast tokens (Canvas / CanvasText
Expand Down
14 changes: 14 additions & 0 deletions src/components/common/TradeDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
import { cn } from '@/lib/utils';
import { formatNumber } from '@/utils/numberFormat.utils';
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';

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

Expand Down Expand Up @@ -54,6 +57,10 @@ const TradeDialog: React.FC<TradeDialogProps> = ({

const title = side === 'buy' ? 'Buy keys' : 'Sell keys';
const confirmLabel = side === 'buy' ? 'Confirm buy' : 'Confirm sell';
const estimatedNetworkFee = formatTransactionFeeDisplay(
TRADE_FEE_ESTIMATE.DEFAULT_NETWORK_FEE,
{ unit: TRADE_FEE_ESTIMATE.UNIT }
);

return (
<Dialog
Expand Down Expand Up @@ -124,6 +131,13 @@ const TradeDialog: React.FC<TradeDialogProps> = ({
/>
)}
</div>
{side === 'buy' && (
<NetworkFeeHint
variant="text"
fee={estimatedNetworkFee}
className="text-white/45"
/>
)}
{side === 'sell' && parsedAmount > availableHoldings && (
<div className="text-xs text-red-300">
You can’t sell more than your current holdings.
Expand Down
5 changes: 5 additions & 0 deletions src/constants/fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ export const KEY_PRICE_BOUNDS = {
MIN_PRICE: 0.001,
MAX_PRICE: 100,
} as const;

export const TRADE_FEE_ESTIMATE = {
DEFAULT_NETWORK_FEE: 0.0001,
UNIT: 'ETH',
} as const;
50 changes: 45 additions & 5 deletions src/pages/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
CREATOR_CARD_ENTRY_CLASS,
creatorCardEntryStyle,
} from '@/utils/cardEntryAnimation.utils';
import { AlertCircle, RefreshCw } from 'lucide-react';
import { AlertCircle, ChevronDown, RefreshCw } from 'lucide-react';
import ClearedFiltersEmptyState from '@/components/common/ClearedFiltersEmptyState';
import CreatorListPagination from '@/components/common/CreatorListPagination';

Expand All @@ -55,6 +55,29 @@ const FEATURED_CREATOR_FACTS = [
];

const FEATURED_CREATOR_FOLLOWER_COUNT: number | null = null;
const FEATURED_CREATOR_KEY_HOLDER_COUNT = 0;

const getFeaturedCreatorKeyHolderCopy = (count: number | null) => {
if (count == null) {
return {
value: 'Key holders unavailable',
explanation: 'Key holder data is not available yet.',
};
}

if (count === 0) {
return {
value: 'No key holders yet',
explanation:
'This creator has not unlocked any key holders yet. Be the first to buy a key and start the collector base.',
};
}

return {
value: `${formatCompactNumber(count)} key holders`,
explanation: 'Number of wallets that currently hold at least one key.',
};
};

// Fallback demo data in case API fails
const DEMO_CREATORS: Course[] = [
Expand Down Expand Up @@ -411,6 +434,9 @@ function LandingPage() {
const start = safePage * PAGE_SIZE;
return filteredCreators.slice(start, start + PAGE_SIZE);
}, [filteredCreators, safePage]);
const featuredCreatorKeyHolderCopy = getFeaturedCreatorKeyHolderCopy(
FEATURED_CREATOR_KEY_HOLDER_COUNT
);

useEffect(() => {
if (pendingScrollRestoreRef.current == null) return;
Expand Down Expand Up @@ -652,8 +678,22 @@ function LandingPage() {
onPageChange={handlePageChange}
className="mt-8"
/>
{safePage >= totalPages - 1 && (
<p
{safePage < totalPages - 1 && (
<div className="mt-4 flex justify-center">
<Button
type="button"
variant="outline"
onClick={() => handlePageChange(safePage + 1)}
aria-label="Load more creators"
className="sr-only rounded-full border-white/10 bg-white/5 px-4 py-2 text-xs font-bold uppercase tracking-[0.18em] text-white shadow-none focus:not-sr-only focus:flex focus:items-center focus:gap-2 focus:outline-none focus:ring-2 focus:ring-amber-400/60 focus:ring-offset-2 focus:ring-offset-slate-950"
>
<ChevronDown className="size-4" aria-hidden="true" />
Load more creators
</Button>
</div>
)}
{safePage >= totalPages - 1 && (
<p
role="status"
aria-live="polite"
className="mt-4 text-center text-xs font-semibold uppercase tracking-[0.18em] text-white/45"
Expand Down Expand Up @@ -765,8 +805,8 @@ function LandingPage() {
/>
<MiniStatChip
label="Audience"
value="12.4K collectors"
explanation="Number of wallets that currently hold at least one of this creator's keys."
value={featuredCreatorKeyHolderCopy.value}
explanation={featuredCreatorKeyHolderCopy.explanation}
/>
<MiniStatChip
label="Access"
Expand Down
31 changes: 31 additions & 0 deletions src/utils/transactionFee.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { formatNumber } from '@/utils/numberFormat.utils';

export interface FormatTransactionFeeOptions {
unit?: string;
maximumFractionDigits?: number;
prefix?: string;
}

/**
* Formats a transaction fee for confirmation UIs.
*
* Keeps the raw value untouched and only normalizes the display with a unit
* suffix and fee-style prefix so tiny network fees stay readable.
*/
export function formatTransactionFeeDisplay(
fee: number | null | undefined,
{
unit = 'ETH',
maximumFractionDigits = 6,
prefix = '~',
}: FormatTransactionFeeOptions = {}
): string {
if (fee == null || !Number.isFinite(fee)) {
return '—';
}

return `${prefix}${formatNumber(fee, {
maximumFractionDigits,
minimumFractionDigits: 0,
})} ${unit}`;
}
Loading