Skip to content
Closed
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
44 changes: 32 additions & 12 deletions src/components/common/TransactionHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,35 @@ import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { ChevronDown, ChevronUp, ArrowUpRight, ArrowDownRight, Minus } from 'lucide-react';
import { cn } from '@/lib/utils';
import { formatRelativeTime } from '@/utils/time.utils';
import { formatCreatorHandle } from '@/utils/handleDisplay.utils';

interface Transaction {
export interface Transaction {
id: string;
type: 'buy' | 'sell';
creator: string;
/** Raw creator identifier from the API — never shown in the UI. */
creatorId: string;
/** Human-readable handle used for display (e.g. instructorId). */
creatorHandle: string;
amount: number;
price: number;
timestamp: number;
txHash: string;
status: 'completed' | 'pending' | 'failed';
}

interface TransactionHistoryProps {
transactions?: Transaction[];
}

const COMPACT_VIEW_KEY = 'accesslayer.transaction-compact-view';

const SAMPLE_TRANSACTIONS: Transaction[] = [
{
id: '1',
type: 'buy',
creator: 'Alex Rivers',
creatorId: '1',
creatorHandle: 'arivers',
amount: 5,
price: 0.05,
timestamp: Date.now() - 1000 * 60 * 30,
Expand All @@ -30,7 +40,8 @@ const SAMPLE_TRANSACTIONS: Transaction[] = [
{
id: '2',
type: 'sell',
creator: 'Sarah Chen',
creatorId: '2',
creatorHandle: 'schen_dev',
amount: 3,
price: 0.12,
timestamp: Date.now() - 1000 * 60 * 60 * 2,
Expand All @@ -40,7 +51,8 @@ const SAMPLE_TRANSACTIONS: Transaction[] = [
{
id: '3',
type: 'buy',
creator: 'Marcus Thorne',
creatorId: '3',
creatorHandle: 'mthorne',
amount: 10,
price: 0.08,
timestamp: Date.now() - 1000 * 60 * 60 * 5,
Expand All @@ -50,7 +62,8 @@ const SAMPLE_TRANSACTIONS: Transaction[] = [
{
id: '4',
type: 'buy',
creator: 'Elena Vance',
creatorId: '4',
creatorHandle: 'evance_design',
amount: 2,
price: 0.04,
timestamp: Date.now() - 1000 * 60 * 60 * 24,
Expand All @@ -60,7 +73,8 @@ const SAMPLE_TRANSACTIONS: Transaction[] = [
{
id: '5',
type: 'sell',
creator: 'David Kojo',
creatorId: '5',
creatorHandle: 'dkojo_beats',
amount: 7,
price: 0.15,
timestamp: Date.now() - 1000 * 60 * 60 * 48,
Expand All @@ -69,9 +83,9 @@ const SAMPLE_TRANSACTIONS: Transaction[] = [
},
];

import { formatRelativeTime } from '@/utils/time.utils';

const TransactionHistory: React.FC = () => {
const TransactionHistory: React.FC<TransactionHistoryProps> = ({
transactions = SAMPLE_TRANSACTIONS,
}) => {
const [isCompact, setIsCompact] = useState(() => {
if (typeof window === 'undefined') return false;
const saved = localStorage.getItem(COMPACT_VIEW_KEY);
Expand Down Expand Up @@ -143,7 +157,8 @@ const TransactionHistory: React.FC = () => {
</div>

<div className="space-y-2">
{SAMPLE_TRANSACTIONS.map(tx => {
{transactions.map(tx => {
const displayHandle = formatCreatorHandle(tx.creatorHandle);
const isExpanded = expandedRows.has(tx.id) || !isCompact;
return (
<div
Expand All @@ -167,7 +182,12 @@ const TransactionHistory: React.FC = () => {
{getTransactionTypeLabel(tx.type)}
</span>
<span className="text-white/40">•</span>
<span className="text-white/90">{tx.creator}</span>
<span
className="text-white/90"
data-testid={`activity-creator-handle-${tx.id}`}
>
{displayHandle}
</span>
</div>
{(!isCompact || isExpanded) && (
<div className="mt-1 flex items-center gap-3 text-xs text-white/50">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen, within } from '@testing-library/react';
import TransactionHistory, {
type Transaction,
} from '@/components/common/TransactionHistory';

const CREATOR_A_ID = '101';
const CREATOR_B_ID = '202';

const twoCreatorTrades: Transaction[] = [
{
id: 'trade-a',
type: 'buy',
creatorId: CREATOR_A_ID,
creatorHandle: 'arivers',
amount: 2,
price: 0.05,
timestamp: Date.now() - 1000 * 60 * 15,
txHash: '0xaaaa...1111',
status: 'completed',
},
{
id: 'trade-b',
type: 'sell',
creatorId: CREATOR_B_ID,
creatorHandle: 'schen_dev',
amount: 1,
price: 0.12,
timestamp: Date.now() - 1000 * 60 * 45,
txHash: '0xbbbb...2222',
status: 'completed',
},
];

beforeEach(() => {
vi.stubEnv('NODE_ENV', 'test');
localStorage.clear();
});

describe('TransactionHistory – activity feed creator handles (integration)', () => {
it('renders the correct creator handle for each trade entry', () => {
render(<TransactionHistory transactions={twoCreatorTrades} />);

expect(
screen.getByTestId('activity-creator-handle-trade-a')
).toHaveTextContent('@arivers');
expect(
screen.getByTestId('activity-creator-handle-trade-b')
).toHaveTextContent('@schen_dev');
});

it('shows different handles for entries from different creators', () => {
render(<TransactionHistory transactions={twoCreatorTrades} />);

const handleA = screen.getByTestId('activity-creator-handle-trade-a');
const handleB = screen.getByTestId('activity-creator-handle-trade-b');

expect(handleA.textContent).not.toBe(handleB.textContent);
expect(handleA).toHaveTextContent('@arivers');
expect(handleB).toHaveTextContent('@schen_dev');
});

it('does not expose raw creator IDs in the rendered output', () => {
const { container } = render(
<TransactionHistory transactions={twoCreatorTrades} />
);

const buyRow = screen.getByTestId('activity-item-buy');
const sellRow = screen.getByTestId('activity-item-sell');

expect(within(buyRow).queryByText(CREATOR_A_ID)).not.toBeInTheDocument();
expect(within(sellRow).queryByText(CREATOR_B_ID)).not.toBeInTheDocument();
expect(container.textContent).not.toContain(CREATOR_A_ID);
expect(container.textContent).not.toContain(CREATOR_B_ID);
});
});
17 changes: 15 additions & 2 deletions src/pages/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,10 @@ const CreatorProfileLoadError: React.FC<CreatorProfileLoadErrorProps> = ({

function LandingPage() {
const [creators, setCreators] = useState<Course[]>([]);
// Creators used for wallet holdings; kept separate from the marketplace
// list so an empty API holdings response can show zero positions while
// the browse grid still falls back to demo creators.
const [holdingsCreators, setHoldingsCreators] = useState<Course[]>([]);
// Last successful fetch timestamp (#301). `null` means we've never
// resolved a load yet — the staleness helper treats that as "stale"
// so the warning surfaces if the load hangs.
Expand Down Expand Up @@ -464,8 +468,10 @@ function LandingPage() {
);
if (data && data.length > 0) {
setCreators(data);
setHoldingsCreators(data);
} else {
setCreators(DEMO_CREATORS);
setHoldingsCreators([]);
}
// Track the last successful fetch so the stale-data warning
// has a baseline to compare against (#301).
Expand Down Expand Up @@ -495,6 +501,7 @@ function LandingPage() {
setShowRetryBanner(false);
setFetchRetryAttempt(0);
setCreators(DEMO_CREATORS);
setHoldingsCreators(DEMO_CREATORS);
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -674,7 +681,7 @@ function LandingPage() {

const heldKeyPositions = useMemo(
() =>
creators.map((creator, index) => ({
holdingsCreators.map((creator, index) => ({
creatorId: creator.id,
quantity:
index === 0
Expand All @@ -685,7 +692,7 @@ function LandingPage() {
isPriceLoading: isPriceRefreshing,
isPriceStale: creatorsAreStale,
})),
[creators, creatorsAreStale, featuredHoldings, isPriceRefreshing]
[holdingsCreators, creatorsAreStale, featuredHoldings, isPriceRefreshing]
);
const portfolioValue = useMemo(
() => calculatePortfolioValue(heldKeyPositions),
Expand Down Expand Up @@ -1163,6 +1170,12 @@ function LandingPage() {
<p className="mt-2 text-xs leading-relaxed text-white/55">
{portfolioValueHelperText}
</p>
<span
data-testid="holdings-header-entry-count"
className="sr-only"
>
{displayedPortfolioValue.heldPositionCount}
</span>
</div>
</div>
{isLoading ? (
Expand Down
Loading