diff --git a/src/components/common/CreatorCard.tsx b/src/components/common/CreatorCard.tsx index f530242a..8b02d65a 100644 --- a/src/components/common/CreatorCard.tsx +++ b/src/components/common/CreatorCard.tsx @@ -19,6 +19,7 @@ import KeySupplyBadge from '@/components/common/KeySupplyBadge'; import CreatorListRowDivider from '@/components/common/CreatorListRowDivider'; import BuyActionHelperText from '@/components/common/BuyActionHelperText'; import CreatorLabeledStatRow from '@/components/common/CreatorLabeledStatRow'; +import { useTransactionTelemetry } from '@/hooks/useTransactionTelemetry'; interface CreatorCardProps { creator: Course; @@ -31,9 +32,11 @@ const CreatorCard: React.FC = ({ creator, className }) => { 'idle' | 'submitting' | 'failed' | 'success' >('idle'); 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}...`); window.setTimeout(() => { @@ -47,6 +50,7 @@ const CreatorCard: React.FC = ({ creator, className }) => { hasFailedOnceRef.current = false; setTransactionState('success'); + trackTransactionEvent('tx_confirmed', { creatorId: creator.id, creatorTitle: creator.title }); showToast.transactionSuccess( 'Purchase Successful!', `You successfully bought a key for ${creator.title}`, diff --git a/src/components/common/EmptyTransactionTimelineState.tsx b/src/components/common/EmptyTransactionTimelineState.tsx new file mode 100644 index 00000000..6d31a800 --- /dev/null +++ b/src/components/common/EmptyTransactionTimelineState.tsx @@ -0,0 +1,25 @@ +import { Button } from '@/components/ui/button'; +import { Clock3 } from 'lucide-react'; + +const EmptyTransactionTimelineState: React.FC = () => { + return ( +
+
+
+ +
+

+ No transactions yet +

+

+ New users will see transaction milestones here after submitting their first buy or sell action. +

+
+ +
+
+
+ ); +}; + +export default EmptyTransactionTimelineState; diff --git a/src/hooks/useTransactionTelemetry.ts b/src/hooks/useTransactionTelemetry.ts new file mode 100644 index 00000000..de04c084 --- /dev/null +++ b/src/hooks/useTransactionTelemetry.ts @@ -0,0 +1,11 @@ +import { useCallback } from 'react'; + +type TelemetryEvent = 'tx_submitted' | 'tx_confirmed'; + +export function useTransactionTelemetry() { + return useCallback((event: TelemetryEvent, metadata: Record) => { + // Scoped client-side telemetry hook for transaction milestones. + // Replace with backend analytics transport when available. + console.info('[telemetry]', event, metadata); + }, []); +} diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index cc0faa10..198b51ad 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -17,6 +17,8 @@ import MarketplaceSection from '@/components/common/MarketplaceSection'; import { ProfileTabPillGroup } from '@/components/common/ProfileTabPill'; import CreatorBreadcrumb from '@/components/common/CreatorBreadcrumb'; import CreatorProfileHeader from '@/components/common/CreatorProfileHeader'; +import TransactionRetryNotice from '@/components/common/TransactionRetryNotice'; +import EmptyTransactionTimelineState from '@/components/common/EmptyTransactionTimelineState'; const FEATURED_CREATOR_FACTS = [ { label: 'Membership', value: 'Collectors Circle' }, @@ -107,11 +109,25 @@ const DEMO_CREATORS: Course[] = [ }, ]; +const CREATOR_SORT_KEY = 'accesslayer.creator-sort'; +const MAX_CREATOR_FETCH_RETRIES = 3; +const BASE_RETRY_DELAY_MS = 800; + +type SortOption = 'featured' | 'price-asc' | 'price-desc' | 'supply-desc'; + function LandingPage() { const [creators, setCreators] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [activeProfileTab, setActiveProfileTab] = useState('overview'); + const [sortOption, setSortOption] = useState(() => { + if (typeof window === 'undefined') return 'featured'; + const saved = window.localStorage.getItem(CREATOR_SORT_KEY) as SortOption | null; + return saved ?? 'featured'; + }); + const [fetchRetryAttempt, setFetchRetryAttempt] = useState(0); + const [showRetryBanner, setShowRetryBanner] = useState(false); + const [finalFetchError, setFinalFetchError] = useState(''); const trimmedSearchQuery = searchQuery.trim(); const hasInvalidSearchInput = /[^a-zA-Z0-9_\s-]/.test(trimmedSearchQuery); @@ -119,9 +135,17 @@ function LandingPage() { ? 'Only letters, numbers, spaces, hyphens, and underscores are supported.' : undefined; + useEffect(() => { + if (typeof window !== 'undefined') { + window.localStorage.setItem(CREATOR_SORT_KEY, sortOption); + } + }, [sortOption]); + useEffect(() => { const fetchCreators = async () => { setIsLoading(true); + setShowRetryBanner(false); + setFinalFetchError(''); try { const data = await courseService.getCourses(); if (data && data.length > 0) { @@ -129,8 +153,24 @@ function LandingPage() { } else { setCreators(DEMO_CREATORS); } - } catch (error) { - console.error('Failed to fetch creators:', error); + setFetchRetryAttempt(0); + } catch { + if (fetchRetryAttempt < MAX_CREATOR_FETCH_RETRIES) { + const nextAttempt = fetchRetryAttempt + 1; + setShowRetryBanner(true); + const backoffDelay = Math.min( + BASE_RETRY_DELAY_MS * 2 ** fetchRetryAttempt, + 5000 + ); + window.setTimeout(() => setFetchRetryAttempt(nextAttempt), backoffDelay); + return; + } + + setFinalFetchError( + 'Unable to load live creators right now. Showing fallback creators.' + ); + setShowRetryBanner(false); + setFetchRetryAttempt(0); setCreators(DEMO_CREATORS); } finally { setTimeout(() => setIsLoading(false), 800); @@ -138,14 +178,14 @@ function LandingPage() { }; fetchCreators(); - }, []); + }, [fetchRetryAttempt]); const filteredCreators = useMemo(() => { if (hasInvalidSearchInput) { return []; } - return creators.filter( + const filtered = creators.filter( creator => creator.title .toLowerCase() @@ -154,7 +194,24 @@ function LandingPage() { .toLowerCase() .includes(trimmedSearchQuery.toLowerCase()) ); - }, [creators, trimmedSearchQuery, hasInvalidSearchInput]); + const sorted = [...filtered]; + switch (sortOption) { + case 'price-asc': + sorted.sort((a, b) => (a.price ?? 0) - (b.price ?? 0)); + break; + case 'price-desc': + sorted.sort((a, b) => (b.price ?? 0) - (a.price ?? 0)); + break; + case 'supply-desc': + sorted.sort( + (a, b) => (b.creatorShareSupply ?? 0) - (a.creatorShareSupply ?? 0) + ); + break; + default: + break; + } + return sorted; + }, [creators, trimmedSearchQuery, hasInvalidSearchInput, sortOption]); const handleResetSearch = () => setSearchQuery(''); @@ -200,12 +257,35 @@ function LandingPage() { onReset={handleResetSearch} showReset={searchQuery.length > 0} > - +
+ +
+ + +
+
@@ -221,10 +301,25 @@ function LandingPage() { {isLoading ? ( ) : filteredCreators.length > 0 ? ( -
- {filteredCreators.map(creator => ( - - ))} +
+ {showRetryBanner && ( + setFetchRetryAttempt(0)} + /> + )} + {finalFetchError && ( +
+ {finalFetchError} +
+ )} +
+ {filteredCreators.map(creator => ( + + ))} +
) : (
@@ -294,6 +389,11 @@ function LandingPage() { />
+ + + + +
);