diff --git a/components/Funding/FundSidebar.tsx b/components/Funding/FundSidebar.tsx
new file mode 100644
index 000000000..669acdd3c
--- /dev/null
+++ b/components/Funding/FundSidebar.tsx
@@ -0,0 +1,22 @@
+'use client';
+
+import { FundingPowerCard } from './FundingPowerCard';
+import { RecentlyVisitedCard, useRecentlyVisited } from './RecentlyVisitedCard';
+import { cn } from '@/utils/styles';
+
+export function FundSidebar() {
+ const recentlyVisited = useRecentlyVisited();
+ const showsRecentlyVisited = recentlyVisited.pages.length > 0;
+
+ return (
+
+
+ {showsRecentlyVisited && (
+
+ )}
+
+ );
+}
diff --git a/components/Funding/FundingPowerCard.tsx b/components/Funding/FundingPowerCard.tsx
new file mode 100644
index 000000000..4ebd6a01d
--- /dev/null
+++ b/components/Funding/FundingPowerCard.tsx
@@ -0,0 +1,226 @@
+'use client';
+
+import Link from 'next/link';
+import { Plus } from 'lucide-react';
+import { RSC_COLORS } from '@/components/ui/icons/ResearchCoinIcon';
+import { Tooltip } from '@/components/ui/Tooltip';
+import { formatCurrency } from '@/utils/currency';
+import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
+import { useExchangeRate } from '@/contexts/ExchangeRateContext';
+import { useUser } from '@/contexts/UserContext';
+import { getAvailableAndPromotionalRscBalance } from '@/components/ResearchCoin/lib/promotionalBalance';
+import { cn } from '@/utils/styles';
+
+interface FundingPowerCardProps {
+ className?: string;
+}
+
+/**
+ * Wallet card for the Activity sidebar. Leads with total funding power,
+ * visualizes the split between RSC and fund-only credits
+ */
+export const FundingPowerCard = ({ className }: FundingPowerCardProps) => {
+ const { user, isLoading: isUserLoading } = useUser();
+ const { showUSD } = useCurrencyPreference();
+ const { exchangeRate, isLoading: isRateLoading } = useExchangeRate();
+
+ const isReady = !isUserLoading && (!showUSD || !isRateLoading);
+
+ if (!isReady) {
+ return ;
+ }
+
+ const canShowUSD = showUSD && exchangeRate > 0;
+
+ const fmt = (rscAmount: number) =>
+ formatCurrency({
+ amount: canShowUSD ? rscAmount * exchangeRate : rscAmount,
+ showUSD: canShowUSD,
+ exchangeRate,
+ shorten: true,
+ skipConversion: true,
+ });
+
+ const balanceRaw = getAvailableAndPromotionalRscBalance(user);
+ const creditsRaw = user?.fundingCredits ?? 0;
+ const total = balanceRaw + creditsRaw;
+ const isEmpty = !user || total === 0;
+
+ const rscWidth = total > 0 ? (balanceRaw / total) * 100 : 0;
+ const creditsWidth = total > 0 ? (creditsRaw / total) * 100 : 0;
+
+ return (
+
+ );
+};
+
+const FundingPowerCardSkeleton = ({ className }: { className?: string }) => (
+
+);
+
+interface SourceRowProps {
+ label: string;
+ tooltip: string;
+ dotColor: string;
+ value: string;
+ valueClassName?: string;
+}
+
+const SourceRow = ({ label, tooltip, dotColor, value, valueClassName }: SourceRowProps) => (
+
+
+
+ {label}
+
+ {value}
+
+
+
+);
+
+interface CtaProps {
+ href: string;
+ children: React.ReactNode;
+ className?: string;
+}
+
+const PrimaryCta = ({ href, children, className }: CtaProps) => (
+
+ {children}
+
+);
+
+const SecondaryCta = ({ href, children, className }: CtaProps) => (
+
+ {children}
+
+);
diff --git a/components/Funding/RecentlyVisitedCard.tsx b/components/Funding/RecentlyVisitedCard.tsx
new file mode 100644
index 000000000..c6f7530a6
--- /dev/null
+++ b/components/Funding/RecentlyVisitedCard.tsx
@@ -0,0 +1,124 @@
+'use client';
+
+import { useCallback, useMemo, useState } from 'react';
+import Link from 'next/link';
+import { useActivityFeed } from '@/hooks/useActivityFeed';
+import { getEntryMeta } from '@/components/Activity/lib/feedEntryAdapters';
+import { cn } from '@/utils/styles';
+
+const MAX_ITEMS = 10;
+
+const ENTRY_TYPE_LABELS: Record = {
+ GRANT: 'Request for Proposal',
+ PREREGISTRATION: 'Proposal',
+ USDFUNDRAISECONTRIBUTION: 'Proposal',
+ PURCHASE: 'Proposal',
+ PAPER: 'Paper',
+ POST: 'Post',
+};
+
+/** Comment/bounty entries point at a document, so label them by that work. */
+const WORK_TYPE_LABELS: Record = {
+ paper: 'Paper',
+ post: 'Post',
+ preregistration: 'Proposal',
+ question: 'Question',
+ discussion: 'Discussion',
+ funding_request: 'Request for Proposal',
+};
+
+interface RecentPage {
+ href: string;
+ title: string;
+ typeLabel?: string;
+}
+
+export interface RecentlyVisited {
+ pages: RecentPage[];
+ clear: () => void;
+}
+
+/**
+ * The viewer's recent pages plus the ability to forget them. Lifted out of the
+ * card so the surrounding column can drop the section entirely once it's
+ * cleared, rather than leaving an empty panel behind.
+ *
+ * Sources the activity feed until real visit tracking exists.
+ */
+export function useRecentlyVisited(): RecentlyVisited {
+ const { entries, isLoading } = useActivityFeed();
+ const [isCleared, setIsCleared] = useState(false);
+
+ const pages = useMemo(() => {
+ const collected: RecentPage[] = [];
+ const seen = new Set();
+
+ for (const entry of entries) {
+ const { title, href } = getEntryMeta(entry);
+ if (!title || !href || seen.has(href)) continue;
+ seen.add(href);
+ const relatedType = entry.relatedWork?.contentType;
+ collected.push({
+ href,
+ title,
+ typeLabel:
+ ENTRY_TYPE_LABELS[entry.contentType] ??
+ (relatedType ? WORK_TYPE_LABELS[relatedType] : undefined),
+ });
+ if (collected.length === MAX_ITEMS) break;
+ }
+
+ return collected;
+ }, [entries]);
+
+ const clear = useCallback(() => setIsCleared(true), []);
+
+ return { pages: isCleared || (isLoading && pages.length === 0) ? [] : pages, clear };
+}
+
+interface RecentlyVisitedCardProps extends RecentlyVisited {
+ className?: string;
+}
+
+/**
+ * Lightweight browsing history for the Activity sidebar: a plain text list of
+ * documents from the activity feed, no thumbnails or metrics.
+ */
+export function RecentlyVisitedCard({ pages, clear, className }: RecentlyVisitedCardProps) {
+ if (pages.length === 0) return null;
+
+ return (
+
+ );
+}
diff --git a/components/Search/SearchSuggestions.tsx b/components/Search/SearchSuggestions.tsx
index e2b9947af..ad1eda2d5 100644
--- a/components/Search/SearchSuggestions.tsx
+++ b/components/Search/SearchSuggestions.tsx
@@ -17,10 +17,14 @@ interface SearchSuggestionsProps {
suggestions?: SearchSuggestion[];
hasLocalSuggestions?: boolean;
clearSearchHistory?: () => void;
+ /** Cap on rendered rows. Defaults to 7 (search modal results). */
+ maxResults?: number;
+ /** When false, omit the Recent / Clear all header (caller owns chrome). */
+ showRecentHeader?: boolean;
}
-// Maximum number of search results to display
-const MAX_RESULTS = 7;
+// Maximum number of search results to display by default
+const DEFAULT_MAX_RESULTS = 7;
// Maximum length for titles before truncating
const MAX_TITLE_LENGTH = 100;
@@ -40,6 +44,8 @@ export function SearchSuggestions({
suggestions = [],
hasLocalSuggestions = false,
clearSearchHistory,
+ maxResults = DEFAULT_MAX_RESULTS,
+ showRecentHeader = true,
}: SearchSuggestionsProps) {
const [erroredSuggestions, setErroredSuggestions] = useState>(new Set());
@@ -275,7 +281,7 @@ export function SearchSuggestions({
return false;
}
})
- .slice(0, MAX_RESULTS); // Limit to maximum number of results
+ .slice(0, maxResults); // Limit to maximum number of results
// Group suggestions by recent vs search results for inline mode
const recentSuggestions = safeSuggestions.filter((s) => s.isRecent);
@@ -291,23 +297,25 @@ export function SearchSuggestions({
{/* Local suggestions section */}
{showSuggestionsOnFocus && !query && hasLocalSuggestions && (
-
-
- Recent
-
-
-
+
+ Recent
+
+
+
+ )}
{safeSuggestions.map(renderSuggestion)}
diff --git a/hooks/useSearchSuggestions.ts b/hooks/useSearchSuggestions.ts
index b7ce55c65..1634f4682 100644
--- a/hooks/useSearchSuggestions.ts
+++ b/hooks/useSearchSuggestions.ts
@@ -1,7 +1,10 @@
import { useState, useEffect, useMemo } from 'react';
import { SearchService } from '@/services/search.service';
import { SearchSuggestion } from '@/types/search';
-import { getSearchHistory, SEARCH_HISTORY_KEY } from '@/utils/searchHistory';
+import {
+ getSearchHistory,
+ clearSearchHistory as clearStoredSearchHistory,
+} from '@/utils/searchHistory';
import { EntityType } from '@/types/search';
interface UseSearchSuggestionsConfig {
@@ -154,7 +157,7 @@ export function useSearchSuggestions({
// Clear all search history
const clearSearchHistory = () => {
if (!includeLocalSuggestions) return;
- localStorage.removeItem(SEARCH_HISTORY_KEY);
+ clearStoredSearchHistory();
setLocalSuggestions([]);
};
diff --git a/utils/searchHistory.ts b/utils/searchHistory.ts
index ac7a79e64..905052948 100644
--- a/utils/searchHistory.ts
+++ b/utils/searchHistory.ts
@@ -26,3 +26,7 @@ export const saveSearchHistory = (items: SearchSuggestion[]) => {
console.error('Error saving to localStorage:', error);
}
};
+
+export const clearSearchHistory = () => {
+ saveSearchHistory([]);
+};