diff --git a/app/layouts/components/NextStepsPanel.tsx b/app/layouts/components/NextStepsPanel.tsx
deleted file mode 100644
index c8c18144b..000000000
--- a/app/layouts/components/NextStepsPanel.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-'use client';
-
-import { memo, useState } from 'react';
-import Link from 'next/link';
-import {
- User,
- GraduationCap,
- MessageCircle,
- CheckCircle,
- Circle,
- Camera,
- FileText,
- Shield,
- BookOpen,
- Info,
- Share2,
- ChevronRight,
- X,
-} from 'lucide-react';
-import { useUser } from '@/contexts/UserContext';
-import { calculateProfileCompletion, ProfileField } from '@/utils/profileCompletion';
-import { cn } from '@/utils/styles';
-import { RadiatingDot } from '@/components/ui/RadiatingDot';
-import { VerifiedBadge } from '@/components/ui/VerifiedBadge';
-
-interface NextStep {
- id: string;
- title: string;
- description: string;
- completed: boolean;
- href: string;
- icon: React.ReactNode;
-}
-
-const NextStepsPanelComponent = () => {
- const { user } = useUser();
- const [isDismissed, setIsDismissed] = useState(false);
-
- // Don't show panel if user is not logged in or has been dismissed
- if (!user || isDismissed) {
- return null;
- }
-
- const profileCompletion = calculateProfileCompletion(user);
- const missing = profileCompletion.missing;
-
- // Define the steps based on missing profile fields
- const steps: NextStep[] = [
- {
- id: 'basic-info',
- title: 'Complete basic info',
- description: 'Photo, education, about',
- completed: ![ProfileField.Photo, ProfileField.Education, ProfileField.About].some((field) =>
- missing.includes(field)
- ),
- href: '/onboarding',
- icon: ,
- },
- {
- id: 'verify-profile',
- title: 'Verify your profile',
- description: 'Improve credibility on platform',
- completed: !missing.includes(ProfileField.Verification),
- href: '/onboarding',
- icon: ,
- },
- {
- id: 'social-presence',
- title: 'Add social presence',
- description: 'LinkedIn, Google Scholar, ...',
- completed: !missing.includes(ProfileField.Social),
- href: '/onboarding',
- icon: ,
- },
- ];
-
- // Show all three steps
- const incompleteSteps = steps;
-
- const handleDismiss = () => {
- setIsDismissed(true);
- };
-
- return (
-
- {/* Close button */}
-
-
-
-
-
-
-
Complete your Profile
-
-
- {/* Progress indicator */}
-
-
- Profile completion
- {profileCompletion.percent}%
-
-
-
-
-
- {incompleteSteps.map((step, index) => (
-
-
- {/* Left side - Icon and content */}
-
- {/* Icon */}
-
- {step.completed ? (
-
- ) : (
-
{step.icon}
- )}
-
-
- {/* Content */}
-
-
- {step.title}
-
-
{step.description}
-
-
-
- {/* Right chevron */}
-
-
-
-
-
- ))}
-
-
- );
-};
-
-export const NextStepsPanel = memo(NextStepsPanelComponent);
-NextStepsPanel.displayName = 'NextStepsPanel';
diff --git a/components/Bounty/BountyMetadataLine.tsx b/components/Bounty/BountyMetadataLine.tsx
deleted file mode 100644
index 85e2350fe..000000000
--- a/components/Bounty/BountyMetadataLine.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { formatDeadline } from '@/utils/date';
-import { CurrencyBadge } from '@/components/ui/CurrencyBadge';
-import { RadiatingDot } from '@/components/ui/RadiatingDot';
-import { ContentTypeBadge } from '@/components/ui/ContentTypeBadge';
-import { Check } from 'lucide-react';
-import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
-
-interface BountyMetadataLineProps {
- amount: number;
- expirationDate?: string;
- isOpen: boolean;
- expiringSoon: boolean;
- className?: string;
- solutionsCount?: number;
- showDeadline?: boolean;
- bountyStatus?: 'OPEN' | 'CLOSED' | 'ASSESSMENT';
- /**
- * If true, the amount is already in the target currency and should not be converted.
- * Useful when the caller has pre-calculated the amount (e.g., Foundation bounty flat fee).
- */
- skipConversion?: boolean;
-}
-
-export const BountyMetadataLine = ({
- amount,
- expirationDate,
- isOpen,
- expiringSoon,
- className = '',
- showDeadline = true,
- bountyStatus,
- skipConversion = false,
-}: BountyMetadataLineProps) => {
- const { showUSD } = useCurrencyPreference();
-
- // Format the deadline text
- const deadlineText =
- bountyStatus === 'ASSESSMENT'
- ? 'Assessment Period'
- : isOpen
- ? expirationDate
- ? formatDeadline(expirationDate)
- : 'No deadline'
- : 'Completed';
-
- return (
-
- {/* Badges and date in one row */}
-
- {/* Badges */}
-
-
-
-
-
- {showDeadline && (
-
- {isOpen ? (
-
- ) : (
-
- )}
-
- {deadlineText}
-
-
- )}
-
-
- );
-};
diff --git a/components/Bounty/index.ts b/components/Bounty/index.ts
deleted file mode 100644
index 9780f0972..000000000
--- a/components/Bounty/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-// Sub-components
-export { BountyMetadataLine } from './BountyMetadataLine';
-export { BountySolutions } from './BountySolutions';
-
-// Bounty feed
-export { BountyFeedItem } from './BountyFeedItem';
-export { BountyWorkCard } from './BountyWorkCard';
-
-// Utils
-export * from './lib/bountyUtil';
diff --git a/components/Earn/EarnRightSidebar.tsx b/components/Earn/EarnRightSidebar.tsx
deleted file mode 100644
index 1a5f634e3..000000000
--- a/components/Earn/EarnRightSidebar.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-'use client';
-
-import React from 'react';
-import Link from 'next/link';
-import { ExternalLink, Vote } from 'lucide-react';
-import { FundingIcon } from '@/components/ui/icons/FundingIcon';
-import { SidebarHeader } from '@/components/ui/SidebarHeader';
-import { Tooltip } from '@/components/ui/Tooltip';
-import Icon from '@/components/ui/icons/Icon';
-
-interface SidebarItem {
- title: string;
- description: string;
- icon: React.ReactNode;
- href?: string;
- external?: boolean;
-}
-
-const useItems: SidebarItem[] = [
- {
- title: 'Fund research',
- description: 'Support open research proposals on the platform.',
- icon: ,
- href: '/fund/proposals',
- },
- {
- title: 'Open Request for Proposal',
- description: 'Fund research',
- icon: ,
- href: '/fund',
- },
- {
- title: 'Tip contributions',
- description: 'Reward authors and reviewers for great contributions.',
- icon: ,
- },
- {
- title: 'Govern the platform',
- description: 'Vote on proposals and shape research incentives.',
- icon: ,
- href: 'https://snapshot.org/#/s:researchhub.eth',
- external: true,
- },
-];
-
-function SidebarRow({ title, description, icon, href, external }: SidebarItem) {
- const tooltipContent = (
-
-
{title}
-
{description}
- {href &&
- (external ? (
-
- Learn more →
-
- ) : (
-
- Learn more
→
-
- ))}
-
- );
-
- return (
-
-
-
- );
-}
-
-export function EarnRightSidebar() {
- return (
-
-
- {/* Use ResearchCoin Section */}
-
-
- {useItems.map((item) => (
-
- ))}
-
-
- {/* Resources Section */}
-
-
-
-
- );
-}
diff --git a/components/Endaoment/index.ts b/components/Endaoment/index.ts
deleted file mode 100644
index b39005b3e..000000000
--- a/components/Endaoment/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export { EndaomentConnectButton } from './EndaomentConnectButton';
-export { EndaomentPaymentButton } from './EndaomentPaymentButton';
-export { EndaomentFundSelector } from './EndaomentFundSelector';
-export { EndaomentInsufficientFundsAlert } from './EndaomentInsufficientFundsAlert';
-export { useConnectEndaoment } from './lib/hooks/useConnectEndaoment';
-export { useEndaomentCallback } from './lib/hooks/useEndaomentCallback';
-export { connectEndaomentAccount, getEndaomentStatus } from '@/services/endaoment.service';
diff --git a/components/Endaoment/lib/hooks/useEndaomentCallback.ts b/components/Endaoment/lib/hooks/useEndaomentCallback.ts
deleted file mode 100644
index 9f1e8863c..000000000
--- a/components/Endaoment/lib/hooks/useEndaomentCallback.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { useEffect, useRef } from 'react';
-import { useSearchParams, usePathname, useRouter } from 'next/navigation';
-import toast from 'react-hot-toast';
-
-const ENDAOMENT_MESSAGES = {
- success: 'Endaoment connected',
- error: 'Unable to connect to Endaoment',
- already_linked: 'This Endaoment account is already linked',
-} as const;
-
-interface UseEndaomentCallbackOptions {
- readonly onSuccess?: () => void;
-}
-
-/**
- * This hook handles the callback from Endaoment after an attempt to connect the user's account.
- * It checks for success or error parameters in the URL and displays appropriate toast notifications.
- * After handling, it cleans up the URL by removing these parameters.
- *
- * @param onSuccess Optional callback to be invoked on successful connection.
- */
-export function useEndaomentCallback({ onSuccess }: UseEndaomentCallbackOptions = {}) {
- const pathname = usePathname();
- const router = useRouter();
- const searchParams = useSearchParams();
- const onSuccessRef = useRef(onSuccess);
- onSuccessRef.current = onSuccess;
-
- const endaomentSuccess = searchParams.get('endaoment_connected') === 'true';
- const endaomentError = searchParams.get('endaoment_error');
-
- useEffect(() => {
- if (!endaomentSuccess && !endaomentError) return;
-
- if (endaomentSuccess) {
- toast.success(ENDAOMENT_MESSAGES.success, { id: 'endaoment-success' });
- onSuccessRef.current?.();
- } else if (endaomentError) {
- const errorMessage =
- ENDAOMENT_MESSAGES[endaomentError as keyof typeof ENDAOMENT_MESSAGES] ||
- decodeURIComponent(endaomentError);
- toast.error(errorMessage, { id: 'endaoment-error' });
- }
-
- const params = new URLSearchParams(searchParams.toString());
- params.delete('endaoment_connected');
- params.delete('endaoment_error');
- const query = params.toString();
- const url = query ? `${pathname}?${query}` : pathname;
- router.replace(url);
- }, [endaomentSuccess, endaomentError, pathname, searchParams, router]);
-}
diff --git a/components/Fund/FundraiseProgressV2.tsx b/components/Fund/FundraiseProgressV2.tsx
deleted file mode 100644
index 32a6c5455..000000000
--- a/components/Fund/FundraiseProgressV2.tsx
+++ /dev/null
@@ -1,393 +0,0 @@
-'use client';
-
-import { FC, useState } from 'react';
-import { Progress } from '@/components/ui/Progress';
-import { AvatarStack } from '@/components/ui/AvatarStack';
-import { ContributorsButton } from '@/components/ui/ContributorsButton';
-import { Clock, Coins } from 'lucide-react';
-import { formatDeadline, formatExactTime, formatDate } from '@/utils/date';
-import { isFundraiseActive } from '@/components/Fund/lib/fundraiseUtils';
-import type { Fundraise } from '@/types/funding';
-import type { Work } from '@/types/work';
-import { Button } from '@/components/ui/Button';
-import { cn } from '@/utils/styles';
-import { CurrencyBadge } from '@/components/ui/CurrencyBadge';
-import { ContributeToFundraiseModal } from '@/components/modals/ContributeToFundraiseModal';
-import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
-import { useShareModalContext } from '@/contexts/ShareContext';
-import { useRouter } from 'next/navigation';
-import { Tooltip } from '@/components/ui/Tooltip';
-import { StatusCard } from '@/components/ui/StatusCard';
-import { colors } from '@/app/styles/colors';
-import { useAuthenticatedAction } from '@/contexts/AuthModalContext';
-
-interface FundraiseProgressProps {
- fundraise: Fundraise;
- fundraiseTitle: string;
- compact?: boolean;
- onContribute?: () => void;
- className?: string;
- onDetailsClick?: () => void;
- /** Work object containing author information */
- work?: Work;
-}
-
-export const FundraiseProgress: FC = ({
- fundraise,
- fundraiseTitle,
- compact = false,
- onContribute,
- className,
- onDetailsClick,
- work,
-}) => {
- const [isContributeModalOpen, setIsContributeModalOpen] = useState(false);
- const { showUSD } = useCurrencyPreference();
- const { showShareModal } = useShareModalContext();
- const router = useRouter();
- const { executeAuthenticatedAction } = useAuthenticatedAction();
-
- if (!fundraise) return null;
-
- const deadlineText = fundraise.endDate ? formatDeadline(fundraise.endDate) : undefined;
- const isEnded = deadlineText?.includes('Ended');
- const isActive = isFundraiseActive(fundraise);
-
- // Calculate progress percentage with a minimum of 5% for visibility
- const progressPercentage =
- fundraise.status === 'COMPLETED'
- ? 100
- : Math.max(
- 0,
- Math.min(100, Math.max(5, (fundraise.amountRaised.rsc / fundraise.goalAmount.rsc) * 100))
- );
-
- // Extract contributors if available
- const contributors =
- fundraise.contributors?.topContributors?.map((contributor) => ({
- profile: {
- profileImage: contributor.authorProfile.profileImage,
- fullName: contributor.authorProfile.fullName,
- id: contributor.authorProfile.id,
- },
- amounts: contributor.totalContribution,
- })) || [];
-
- // Get status display for the non-compact view
- const getStatusDisplay = () => {
- switch (fundraise.status) {
- case 'COMPLETED':
- return (
-
- Fundraise Completed
-
- );
- case 'CLOSED':
- return (
-
- Fundraise Closed
-
- );
- case 'OPEN':
- if (isEnded) {
- return Ended
;
- }
- if (!deadlineText || !fundraise.endDate) {
- return null;
- }
- return (
-
-
-
-
- {deadlineText}
-
- );
- default:
- return null;
- }
- };
-
- const handleContributeClick = () => {
- executeAuthenticatedAction(() => {
- setIsContributeModalOpen(true);
- if (onContribute) {
- onContribute();
- }
- });
- };
-
- const handleContributeSuccess = () => {
- // Close the modal
- setIsContributeModalOpen(false);
-
- // Call the parent onContribute callback if provided
- if (onContribute) {
- onContribute();
- }
-
- showShareModal({
- url: window.location.href,
- docTitle: fundraiseTitle,
- action: 'USER_FUNDED_PROPOSAL',
- });
-
- router.refresh();
- };
-
- const getProgressVariant = () => {
- if (fundraise.status === 'COMPLETED') {
- return 'success';
- } else if (isEnded) {
- return 'gray';
- }
- return 'default';
- };
-
- return (
- <>
-
- {compact ? (
-
- {/* Main row: Amount + Status + Contributors + Buttons */}
-
-
- {/* Amount raised / goal */}
-
-
- /
-
-
-
- {/* Deadline - hidden on mobile */}
- {fundraise.endDate && isActive && (
-
-
- Ends {formatDate(fundraise.endDate)}
-
- )}
-
- {/* Status badges */}
- {fundraise.status === 'COMPLETED' && (
-
- Completed
-
- )}
- {fundraise.status === 'CLOSED' && (
-
- Closed
-
- )}
- {fundraise.status === 'OPEN' && isEnded && (
-
- Ended
-
- )}
- {isActive && (
-
- Open
-
- )}
-
- {/* Contributors inline (hidden on mobile) */}
- {contributors.length > 0 && (
-
-
({
- src: contributor.profile.profileImage || '',
- alt: contributor.profile.fullName,
- tooltip: contributor.profile.fullName,
- authorId: contributor.profile.id,
- }))}
- size="xs"
- maxItems={3}
- spacing={-6}
- showExtraCount={false}
- totalItemsCount={contributors.length}
- extraCountLabel="Funders"
- showLabel={false}
- />
-
- {contributors.length} {contributors.length === 1 ? 'Funder' : 'Funders'}
-
-
- )}
-
-
- {/* CTA Buttons */}
-
- {isActive ? (
-
- Fund
-
- ) : onDetailsClick ? (
- {
- e.stopPropagation();
- onDetailsClick();
- }}
- className="!py-1.5 !px-2.5 text-gray-600 hover:text-gray-800"
- >
- Details
-
- ) : null}
-
-
-
- {/* Progress bar */}
-
-
- ) : (
-
-
-
-
-
- raised of
-
- goal
-
-
{getStatusDisplay()}
-
-
-
-
-
- {isActive ? (
-
-
- Fund this research
-
- ) : onDetailsClick ? (
-
{
- e.stopPropagation();
- onDetailsClick();
- }}
- >
- Details
-
- ) : null}
-
- {contributors.length > 0 && (
-
-
-
- )}
-
-
- )}
-
-
- {/* Contribute Modal */}
- setIsContributeModalOpen(false)}
- onContributeSuccess={handleContributeSuccess}
- fundraise={fundraise}
- proposalTitle={fundraiseTitle}
- work={work}
- />
- >
- );
-};
diff --git a/components/Fund/GrantInfo.tsx b/components/Fund/GrantInfo.tsx
deleted file mode 100644
index 8dd611fae..000000000
--- a/components/Fund/GrantInfo.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-'use client';
-
-import { FC } from 'react';
-import { CurrencyBadge } from '@/components/ui/CurrencyBadge';
-import { Button } from '@/components/ui/Button';
-import { AvatarStack } from '@/components/ui/AvatarStack';
-import { formatDate, isDeadlineInFuture } from '@/utils/date';
-import { FeedGrantContent } from '@/types/feed';
-import { useRouter } from 'next/navigation';
-import { colors } from '@/app/styles/colors';
-import { StatusCard } from '@/components/ui/StatusCard';
-import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
-import { Clock } from 'lucide-react';
-import { buildWorkUrl } from '@/utils/url';
-
-interface GrantInfoProps {
- grant: FeedGrantContent;
- className?: string;
- onFeedItemClick?: () => void;
-}
-
-export const GrantInfo: FC = ({ grant, className, onFeedItemClick }) => {
- const router = useRouter();
- const { showUSD } = useCurrencyPreference();
-
- if (!grant || !grant.grant) return null;
-
- // Check if RFP is active
- const isActive =
- grant.grant?.status === 'OPEN' &&
- (grant.grant?.endDate ? isDeadlineInFuture(grant.grant?.endDate) : true);
- const deadline = grant.grant.endDate ? formatDate(grant.grant.endDate) : undefined;
-
- const applicants =
- grant.grant.applicants?.map(({ profile }) => ({
- profile: {
- profileImage: profile.profileImage,
- fullName:
- profile.firstName && profile.lastName
- ? `${profile.firstName} ${profile.lastName}`
- : profile.firstName || 'Applicant',
- id: profile.id,
- },
- amount: 0,
- })) || [];
-
- const grantUrl = buildWorkUrl({
- id: grant.id,
- slug: grant.slug,
- contentType: 'funding_request',
- });
-
- const navigateToGrant = (e: React.MouseEvent) => {
- e.stopPropagation();
- onFeedItemClick?.();
- router.push(grantUrl);
- };
-
- const budgetAmount = grant.grant.amount?.rsc || 0;
- const applicantCount = applicants.length;
-
- return (
-
-
- {/* Left: Amount + Deadline + Applicants */}
-
-
-
- {/* Close date */}
- {deadline && isActive && (
-
-
- Closes {deadline}
-
- )}
-
- {/* Status badges */}
- {isActive && (
-
- Open
-
- )}
- {!isActive && (
-
- Closed
-
- )}
-
- {/* Applicants section */}
- {applicantCount > 0 && (
-
-
({
- src: applicant.profile.profileImage || '',
- alt: applicant.profile.fullName,
- tooltip: applicant.profile.fullName,
- authorId: applicant.profile.id,
- }))}
- size="xs"
- maxItems={3}
- spacing={-6}
- showExtraCount={false}
- totalItemsCount={applicantCount}
- extraCountLabel="Applicants"
- showLabel={false}
- />
-
- {applicantCount} {applicantCount === 1 ? 'Applicant' : 'Applicants'}
-
-
- )}
-
-
- {/* Right: CTA Buttons */}
-
-
- Details
-
- {isActive && (
-
- Apply
-
- )}
-
-
-
- );
-};
diff --git a/components/Fund/lib/FundUtils.ts b/components/Fund/lib/FundUtils.ts
deleted file mode 100644
index 978ca6c87..000000000
--- a/components/Fund/lib/FundUtils.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-interface Update {
- id: number;
- createdDate: string;
- content?: any;
-}
-
-/**
- * Calculate the update rate as a percentage of months with updates in the last 12 months
- * Only the first update in each month counts towards the rate
- * @param updates - Array of updates with createdDate
- * @returns Percentage (0-100) representing how many months had updates
- */
-export const calculateUpdateRate = (updates: Update[]): number => {
- if (updates.length === 0) {
- return 0;
- }
-
- const now = new Date();
- const updatesInLast12Months = updates.filter((update) => {
- const updateDate = new Date(update.createdDate);
- const monthsDiff =
- (now.getFullYear() - updateDate.getFullYear()) * 12 +
- (now.getMonth() - updateDate.getMonth());
- return monthsDiff <= 12;
- });
-
- // Group updates by month-year and only count unique months
- const uniqueMonths = new Set();
-
- updatesInLast12Months.forEach((update) => {
- const updateDate = new Date(update.createdDate);
- const monthYear = `${updateDate.getFullYear()}-${updateDate.getMonth()}`;
- uniqueMonths.add(monthYear);
- });
-
- // Calculate percentage based on unique months with updates out of 12 months
- return Math.round((uniqueMonths.size / 12) * 100);
-};
diff --git a/components/Funding/FundingDirectionIcon.tsx b/components/Funding/FundingDirectionIcon.tsx
deleted file mode 100644
index 2e477b78d..000000000
--- a/components/Funding/FundingDirectionIcon.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { cn } from '@/utils/styles';
-import { FUNDING_DIRECTION, type FundingDirection } from './fundingDirection';
-
-interface FundingDirectionIconProps {
- readonly direction: FundingDirection;
- /**
- * Coloured by direction, or neutral: a grey circle whose arrow inherits
- * the text colour around it, for a place that colours itself (a tab).
- */
- readonly colored?: boolean;
- readonly className?: string;
- readonly iconClassName?: string;
-}
-
-/** The direction's arrow in a circle. */
-export function FundingDirectionIcon({
- direction,
- colored = true,
- className,
- iconClassName,
-}: FundingDirectionIconProps) {
- const { icon: Icon, iconClass, chipClass } = FUNDING_DIRECTION[direction];
- return (
-
-
-
- );
-}
diff --git a/components/Funding/GrantActionBar.tsx b/components/Funding/GrantActionBar.tsx
deleted file mode 100644
index cbf871eb0..000000000
--- a/components/Funding/GrantActionBar.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-'use client';
-
-import { useState, useCallback } from 'react';
-import { ArrowUp, ArrowDown, Share2, MoreHorizontal, Flag } from 'lucide-react';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faBookmark } from '@fortawesome/free-regular-svg-icons';
-import { faBookmark as faBookmarkSolid } from '@fortawesome/free-solid-svg-icons';
-import { Work } from '@/types/work';
-import { useAuthenticatedAction } from '@/contexts/AuthModalContext';
-import { useVote } from '@/hooks/useVote';
-import { useUserVotes } from '@/hooks/useUserVotes';
-import { useShareModalContext } from '@/contexts/ShareContext';
-import { useIsInList } from '@/components/UserList/lib/hooks/useIsInList';
-import { useAddToList } from '@/components/UserList/lib/UserListsContext';
-import { AddToListModal } from '@/components/UserList/AddToListModal';
-import { FlagContentModal } from '@/components/modals/FlagContentModal';
-import { BaseMenu, BaseMenuItem } from '@/components/ui/form/BaseMenu';
-import { Button } from '@/components/ui/Button';
-import { cn } from '@/utils/styles';
-import toast from 'react-hot-toast';
-
-interface GrantActionBarProps {
- work: Work;
- className?: string;
-}
-
-export function GrantActionBar({ work, className }: GrantActionBarProps) {
- const [isAddToListModalOpen, setIsAddToListModalOpen] = useState(false);
- const [isFlagModalOpen, setIsFlagModalOpen] = useState(false);
- const { executeAuthenticatedAction } = useAuthenticatedAction();
- const { showShareModal } = useShareModalContext();
-
- const { vote, isVoting } = useVote({
- votableEntityId: work.id,
- feedContentType: 'POST',
- relatedDocumentId: work.id.toString(),
- relatedDocumentContentType: work.contentType,
- });
-
- const [voteCount, setVoteCount] = useState(
- work.metrics?.adjustedScore ?? work.metrics?.votes ?? 0
- );
-
- const {
- data: userVotes,
- isLoading: isLoadingVotes,
- refresh: refreshVotes,
- } = useUserVotes({
- paperIds: [],
- postIds: [work.id],
- });
-
- const isUpvoted = userVotes?.posts[work.id]?.voteType === 'upvote';
- const isDownvoted = userVotes?.posts[work.id]?.voteType === 'downvote';
-
- const { isInList } = useIsInList(work.unifiedDocumentId);
- const { isTogglingDefaultList, handleAddToList } = useAddToList({
- unifiedDocumentId: work.unifiedDocumentId,
- isInList,
- onOpenModal: () => setIsAddToListModalOpen(true),
- });
-
- const handleVote = useCallback(
- async (direction: 'up' | 'down') => {
- const wasUpvoted = isUpvoted;
- const wasDownvoted = isDownvoted;
-
- try {
- let newVoteType: 'UPVOTE' | 'DOWNVOTE' | 'NEUTRAL';
- let countDelta: number;
-
- if (direction === 'up') {
- if (wasUpvoted) {
- newVoteType = 'NEUTRAL';
- countDelta = -1;
- } else if (wasDownvoted) {
- newVoteType = 'UPVOTE';
- countDelta = 2;
- } else {
- newVoteType = 'UPVOTE';
- countDelta = 1;
- }
- } else {
- if (wasDownvoted) {
- newVoteType = 'NEUTRAL';
- countDelta = 1;
- } else if (wasUpvoted) {
- newVoteType = 'DOWNVOTE';
- countDelta = -2;
- } else {
- newVoteType = 'DOWNVOTE';
- countDelta = -1;
- }
- }
-
- await vote(newVoteType);
- setVoteCount((prev) => prev + countDelta);
- await refreshVotes();
- } catch (error: any) {
- if (
- error?.status === 403 ||
- error?.response?.status === 403 ||
- error?.detail === 'Can not vote on own content'
- ) {
- toast.error('Cannot vote on your own content');
- } else {
- toast.error('Unable to process your vote. Please try again.');
- }
- }
- },
- [isUpvoted, isDownvoted, vote, refreshVotes]
- );
-
- return (
- <>
-
- {/* Vote controls */}
-
-
executeAuthenticatedAction(() => handleVote('up'))}
- disabled={isVoting || isLoadingVotes}
- className={cn(
- 'px-3 py-2 rounded-l-lg transition-colors',
- isUpvoted ? 'text-green-600 hover:bg-green-100' : 'text-gray-600 hover:bg-gray-100',
- isVoting || isLoadingVotes ? 'cursor-not-allowed' : 'cursor-pointer'
- )}
- aria-label="Upvote"
- >
-
-
-
- {voteCount}
-
-
executeAuthenticatedAction(() => handleVote('down'))}
- disabled={isVoting || isLoadingVotes}
- className={cn(
- 'px-3 py-2 rounded-r-lg transition-colors',
- isDownvoted ? 'text-red-600 hover:bg-red-100' : 'text-gray-600 hover:bg-gray-100',
- isVoting || isLoadingVotes ? 'cursor-not-allowed' : 'cursor-pointer'
- )}
- aria-label="Downvote"
- >
-
-
-
-
- {/* Save to list */}
- {work.unifiedDocumentId && (
-
-
-
- )}
-
- {/* Share */}
-
- showShareModal({
- url: window.location.href,
- docTitle: work.title,
- action: 'USER_SHARED_DOCUMENT',
- shouldShowConfetti: false,
- })
- }
- className="flex items-center px-3 py-2 bg-gray-50 text-gray-600 rounded-lg hover:bg-gray-100 transition-colors"
- aria-label="Share"
- >
-
-
-
- {/* More options */}
-
-
-
- }
- >
- executeAuthenticatedAction(() => setIsFlagModalOpen(true))}>
-
- Flag Content
-
-
-
-
- {/* Modals */}
- {work.unifiedDocumentId && (
- setIsAddToListModalOpen(false)}
- unifiedDocumentId={work.unifiedDocumentId}
- />
- )}
-
- setIsFlagModalOpen(false)}
- documentId={work.id.toString()}
- workType={work.contentType}
- />
- >
- );
-}
diff --git a/components/Funding/GrantCarousel.tsx b/components/Funding/GrantCarousel.tsx
deleted file mode 100644
index 7c6271153..000000000
--- a/components/Funding/GrantCarousel.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-'use client';
-
-import { FC, useRef, useState, useEffect } from 'react';
-import Link from 'next/link';
-import { FeedEntry, FeedGrantContent } from '@/types/feed';
-import { getGrantBadgeAmount } from '@/types/grant';
-import { Carousel } from '@/components/ui/Carousel';
-import { FundingProposalCard } from './FundingProposalCard';
-import { ProposalCardSkeleton } from '@/components/skeletons/ProposalCardSkeleton';
-import { cn } from '@/utils/styles';
-import { ArrowRight, RefreshCw } from 'lucide-react';
-import { RadiatingDot } from '@/components/ui/RadiatingDot';
-import { useFeed } from '@/hooks/useFeed';
-import { getShortTitle } from './lib/getShortTitle';
-import { buildWorkUrl } from '@/utils/url';
-
-interface GrantCarouselProps {
- grant: FeedEntry;
- className?: string;
- isClosed?: boolean;
- isDashboard?: boolean;
-}
-
-export { getShortTitle } from './lib/getShortTitle';
-
-function formatCompactUSD(usd: number): string {
- if (usd >= 1_000_000) return `$${(usd / 1_000_000).toFixed(2).replace(/\.?0+$/, '')}M`;
- if (usd >= 1_000) return `$${(usd / 1_000).toFixed(0)}K`;
- return `$${Math.round(usd).toLocaleString()}`;
-}
-
-export const GrantCarousel: FC = ({
- grant,
- className,
- isClosed,
- isDashboard,
-}) => {
- const content = grant.content as FeedGrantContent;
- const grantData = content.grant;
- const grantHref = buildWorkUrl({
- id: content.id,
- slug: content.slug,
- contentType: 'funding_request',
- });
-
- const sectionRef = useRef(null);
- const [hasBeenVisible, setHasBeenVisible] = useState(false);
-
- useEffect(() => {
- const el = sectionRef.current;
- if (!el) return;
-
- const observer = new IntersectionObserver(
- ([entry]) => {
- if (entry.isIntersecting) {
- setHasBeenVisible(true);
- observer.disconnect();
- }
- },
- { rootMargin: '200px' }
- );
- observer.observe(el);
- return () => observer.disconnect();
- }, []);
-
- const { entries, isLoading, hasMore, loadMore } = useFeed(
- 'all',
- hasBeenVisible
- ? {
- endpoint: 'funding_feed',
- contentType: 'PREREGISTRATION',
- grantId: grantData.id,
- ordering: 'best',
- }
- : {
- endpoint: 'funding_feed',
- contentType: 'PREREGISTRATION',
- initialData: { entries: [], hasMore: false },
- }
- );
-
- const hasFetchedProposals = hasBeenVisible && (entries.length > 0 || !isLoading);
- const showSkeleton = !hasFetchedProposals;
- const badgeUsd = getGrantBadgeAmount(grantData).usd;
-
- return (
-
- {/* Title + badge */}
-
-
-
- {getShortTitle(content.grant.shortTitle, content.title)}
-
-
- {badgeUsd > 0 && (
-
- {formatCompactUSD(badgeUsd)} pool
-
- )}
-
-
- {/* Metadata row */}
-
- {(grantData.organization || content.createdBy?.fullName) && (
- <>
-
{grantData.organization || content.createdBy.fullName}
-
•
- >
- )}
- {isClosed ? (
-
-
- Closed
-
- ) : (
-
-
- Rolling fund
-
- )}
- {entries.length > 0 && (
- <>
-
•
-
- {entries.length} proposals
-
- >
- )}
-
- {!isClosed && entries.length > 0 && !isDashboard && (
-
- Learn more
-
- )}
-
-
- {/* Carousel of proposals */}
-
- {showSkeleton ? (
-
- {[1, 2, 3].map((i) => (
-
- ))}
-
- ) : entries.length > 0 ? (
-
- {entries.map((entry) => (
-
-
-
- ))}
- {isLoading && (
-
- )}
-
- ) : isDashboard ? (
-
- ) : (
-
-
-
No proposals yet
-
- Learn more
-
-
-
- )}
-
-
- );
-};
diff --git a/components/Funding/GrantList.tsx b/components/Funding/GrantList.tsx
deleted file mode 100644
index 783d6b8b5..000000000
--- a/components/Funding/GrantList.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-'use client';
-
-import { FC } from 'react';
-import Link from 'next/link';
-import { Plus } from 'lucide-react';
-import { GrantCarousel } from '@/components/Funding/GrantCarousel';
-import { FeedEntry } from '@/types/feed';
-
-interface GrantListProps {
- grants: FeedEntry[];
- closedGrants?: FeedEntry[];
- emptyMessage?: string;
- showCreateCTA?: boolean;
- isDashboard?: boolean;
-}
-
-export const GrantList: FC = ({
- grants,
- closedGrants,
- emptyMessage = 'No grants found.',
- showCreateCTA = false,
- isDashboard,
-}) => {
- const hasClosedGrants = closedGrants && closedGrants.length > 0;
-
- if (grants.length === 0 && !hasClosedGrants) {
- return (
-
-
{emptyMessage}
- {showCreateCTA && (
-
-
- Open a request for proposal
-
- )}
-
- );
- }
-
- return (
-
- {grants.map((grant) => (
-
- ))}
-
- {showCreateCTA && (
-
-
-
-
- Open a request for proposal
-
-
-
- )}
-
- {hasClosedGrants &&
- closedGrants.map((grant) =>
)}
-
- );
-};
diff --git a/components/Funding/fundingDirection.ts b/components/Funding/fundingDirection.ts
index 36fb10c26..f5d14c3c6 100644
--- a/components/Funding/fundingDirection.ts
+++ b/components/Funding/fundingDirection.ts
@@ -1,41 +1,2 @@
-import { ArrowDownLeft, ArrowUpRight, type LucideIcon } from 'lucide-react';
-
-/**
- * Which way money moves for the person looking at it: out of their hands
- * when they fund research, into them when their research is funded. One
- * visual language for it everywhere — the My Funding tabs, the workspace's
- * intent toggle, a document's kind in a list.
- */
-export type FundingDirection = 'giving' | 'receiving';
-
/** What a workspace conversation is for; the backend stores the same values. */
export type FundingIntent = 'fund' | 'need_funding';
-
-export interface FundingDirectionStyle {
- readonly icon: LucideIcon;
- /** The arrow, when it is coloured rather than inheriting. */
- readonly iconClass: string;
- /** The circle behind the arrow, when coloured. */
- readonly chipClass: string;
- /** Text that takes the direction's colour, e.g. an active toggle label. */
- readonly textClass: string;
-}
-
-/** Money out reads blue, the brand's default; money in reads emerald. */
-export const FUNDING_DIRECTION: Record = {
- giving: {
- icon: ArrowUpRight,
- iconClass: 'text-primary-700',
- chipClass: 'bg-primary-100',
- textClass: 'text-primary-700',
- },
- receiving: {
- icon: ArrowDownLeft,
- iconClass: 'text-emerald-700',
- chipClass: 'bg-emerald-100',
- textClass: 'text-emerald-700',
- },
-};
-
-export const directionForIntent = (intent: FundingIntent): FundingDirection =>
- intent === 'fund' ? 'giving' : 'receiving';
diff --git a/components/Funding/lib/getShortTitle.ts b/components/Funding/lib/getShortTitle.ts
deleted file mode 100644
index 03b6a45bd..000000000
--- a/components/Funding/lib/getShortTitle.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export function getShortTitle(shortTitle: string, title: string): string {
- if (shortTitle) return shortTitle;
- return title.replace(/^Request for Proposals\s*[-–—:]\s*/i, '');
-}
diff --git a/components/Grant/GrantInfo.tsx b/components/Grant/GrantInfo.tsx
deleted file mode 100644
index 1975a8ad1..000000000
--- a/components/Grant/GrantInfo.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-'use client';
-
-import { FC } from 'react';
-import { CurrencyBadge } from '@/components/ui/CurrencyBadge';
-import { Button } from '@/components/ui/Button';
-import { AvatarStack } from '@/components/ui/AvatarStack';
-import { formatDate, isDeadlineInFuture } from '@/utils/date';
-import { FeedGrantContent } from '@/types/feed';
-import { GRANT_STATUS_CONFIG } from '@/types/grant';
-import { useRouter } from 'next/navigation';
-import { colors } from '@/app/styles/colors';
-import { StatusCard } from '@/components/ui/StatusCard';
-import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
-import { Clock } from 'lucide-react';
-import { buildWorkUrl } from '@/utils/url';
-
-interface GrantInfoProps {
- grant: FeedGrantContent;
- className?: string;
- onFeedItemClick?: () => void;
-}
-
-export const GrantInfo: FC = ({ grant, className, onFeedItemClick }) => {
- const router = useRouter();
- const { showUSD } = useCurrencyPreference();
-
- if (!grant || !grant.grant) return null;
-
- const isActive =
- grant.grant.status === 'OPEN' &&
- (grant.grant.endDate ? isDeadlineInFuture(grant.grant.endDate) : true);
- const deadline = grant.grant.endDate ? formatDate(grant.grant.endDate) : undefined;
- const statusBadge = GRANT_STATUS_CONFIG[grant.grant.status];
-
- const applicants =
- grant.grant.applicants?.map(({ profile }) => ({
- profile: {
- profileImage: profile.profileImage,
- fullName:
- profile.firstName && profile.lastName
- ? `${profile.firstName} ${profile.lastName}`
- : profile.firstName || 'Applicant',
- id: profile.id,
- },
- amount: 0,
- })) || [];
-
- const grantUrl = buildWorkUrl({
- id: grant.id,
- slug: grant.slug,
- contentType: 'funding_request',
- });
-
- const navigateToGrant = (e: React.MouseEvent) => {
- e.stopPropagation();
- onFeedItemClick?.();
- router.push(grantUrl);
- };
-
- const budgetAmount = grant.grant.amount?.rsc || 0;
- const applicantCount = applicants.length;
-
- return (
-
-
- {/* Left: Amount + Deadline + Applicants */}
-
-
-
- {/* Close date */}
- {deadline && isActive && (
-
-
- Closes {deadline}
-
- )}
-
- {/* Status badge */}
-
- {isActive ? 'Open' : statusBadge.label}
-
-
- {/* Applicants section */}
- {applicantCount > 0 && (
-
-
({
- src: applicant.profile.profileImage || '',
- alt: applicant.profile.fullName,
- tooltip: applicant.profile.fullName,
- authorId: applicant.profile.id,
- }))}
- size="xs"
- maxItems={3}
- spacing={-6}
- showExtraCount={false}
- totalItemsCount={applicantCount}
- extraCountLabel="Applicants"
- showLabel={false}
- />
-
- {applicantCount} {applicantCount === 1 ? 'Applicant' : 'Applicants'}
-
-
- )}
-
-
- {/* Right: CTA Buttons */}
-
-
- Details
-
- {isActive && (
-
- Apply
-
- )}
-
-
-
- );
-};
diff --git a/components/Leaderboard/LeaderboardOverview.tsx b/components/Leaderboard/LeaderboardOverview.tsx
deleted file mode 100644
index bf1f2f062..000000000
--- a/components/Leaderboard/LeaderboardOverview.tsx
+++ /dev/null
@@ -1,286 +0,0 @@
-'use client';
-
-import { useEffect } from 'react';
-import { TopReviewer, TopFunder } from '@/types/leaderboard';
-import { Avatar } from '@/components/ui/Avatar';
-import { CurrencyBadge } from '@/components/ui/CurrencyBadge';
-import Link from 'next/link';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faWreathLaurel } from '@fortawesome/pro-light-svg-icons';
-import { AuthorTooltip } from '@/components/ui/AuthorTooltip';
-import { VerifiedBadge } from '@/components/ui/VerifiedBadge';
-import { navigateToAuthorProfile } from '@/utils/navigation';
-import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
-import { useLeaderboard } from '@/contexts/LeaderboardContext';
-import { useUser } from '@/contexts/UserContext';
-import { cn } from '@/utils/styles';
-import { Star, ChevronRight } from 'lucide-react';
-import { Icon } from '@/components/ui/icons/Icon';
-
-const LeaderboardListSkeleton = () => (
-
- {[...Array(5)].map((_, i) => (
-
- ))}
-
-);
-
-// Skeleton Loader for leaderboard sections
-export const LeaderboardSkeleton = () => (
- <>
- {/* Top Funders Section Skeleton */}
-
-
- {/* Divider */}
-
-
- {/* Top Peer Reviewers Section Skeleton */}
-
-
-
-
-
-
Top Peer Reviewers
-
-
-
- View All
-
-
-
-
-
-
- >
-);
-
-export const LeaderboardOverview = () => {
- const { data, isLoading, error, fetchData } = useLeaderboard();
- const { showUSD } = useCurrencyPreference();
- const { user } = useUser();
- const currentUserAuthorId = user?.authorProfile?.id;
-
- const reviewers = data?.reviewers || [];
- const funders = data?.funders || [];
-
- useEffect(() => {
- fetchData();
- }, [fetchData]);
-
- // Helper to render rank with icon
- const renderRank = (rank: number) => {
- if (rank === 1) {
- return (
-
-
- 1
-
- );
- }
- return (
-
- {rank}
-
- );
- };
-
- const renderListItem = (
- item: TopReviewer | TopFunder,
- index: number,
- type: 'reviewer' | 'funder',
- isCurrentUser: boolean
- ) => {
- const rank = index + 1;
- const authorId = item.authorProfile?.id;
- const amount =
- type === 'reviewer' ? (item as TopReviewer).earnedRsc : (item as TopFunder).totalFunding;
-
- const displayName = item.authorProfile.fullName;
- // Show gradient for long names, or shorter names if there's a verified badge (which takes up space)
- const needsGradient = displayName.length > 15 || (item.isVerified && displayName.length > 12);
-
- return (
- authorId && navigateToAuthorProfile(authorId)}
- className={cn(
- 'grid grid-cols-[32px_40px_1fr_auto] gap-x-2 items-center px-1 py-2 rounded-md cursor-pointer',
- isCurrentUser ? 'bg-orange-50 hover:bg-orange-100' : 'hover:bg-gray-50'
- )}
- >
- {/* Rank */}
-
{renderRank(rank)}
-
- {/* Avatar */}
-
- {authorId ? (
-
-
-
- ) : (
-
- )}
-
-
- {/* Name */}
-
-
- {authorId ? (
-
-
- {displayName}
-
-
- ) : (
-
- {displayName}
-
- )}
- {needsGradient && (
-
- )}
-
- {item.isVerified && (
-
-
-
- )}
-
-
- {/* RSC Badge */}
-
-
-
-
- );
- };
-
- return (
- <>
- {/* Top Funders Section */}
-
-
- {isLoading ? (
-
- ) : error ? (
-
{error}
- ) : funders.length > 0 ? (
-
- {funders.map((funder, index) =>
- renderListItem(
- funder,
- index,
- 'funder',
- funder.authorProfile?.id === currentUserAuthorId
- )
- )}
-
- ) : (
-
No funders found this month.
- )}
-
-
- {/* Divider */}
-
-
- {/* Top Peer Reviewers Section */}
-
-
-
-
-
-
Top Peer Reviewers
-
-
-
- View All
-
-
-
- {isLoading ? (
-
- ) : error ? (
-
{error}
- ) : reviewers.length > 0 ? (
-
- {reviewers.map((reviewer, index) =>
- renderListItem(
- reviewer,
- index,
- 'reviewer',
- reviewer.authorProfile?.id === currentUserAuthorId
- )
- )}
-
- ) : (
-
No reviewers found this week.
- )}
- {/* Secondary CTA for preprints needing review */}
-
- View preprints needing review
-
-
- >
- );
-};
diff --git a/components/Notebook/LeftSidebar/OrganizationSwitcher.tsx b/components/Notebook/LeftSidebar/OrganizationSwitcher.tsx
deleted file mode 100644
index 3ea0e68f8..000000000
--- a/components/Notebook/LeftSidebar/OrganizationSwitcher.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-'use client';
-
-import { ChevronDown, Settings, Check, Lock } from 'lucide-react';
-import { Avatar } from '@/components/ui/Avatar';
-import { Button } from '@/components/ui/Button';
-import type { Organization } from '@/types/organization';
-import { OrganizationSwitcherSkeleton } from '@/components/skeletons/OrganizationSwitcherSkeleton';
-import { BaseMenu, BaseMenuItem } from '@/components/ui/form/BaseMenu';
-import React, { useState } from 'react';
-import { OrganizationSettingsModal } from '@/components/modals/OrganizationSettingsModal';
-
-interface OrganizationSwitcherProps {
- organizations: Organization[];
- selectedOrg: Organization | null;
- onOrgSelect: (org: Organization) => Promise;
- isLoading?: boolean;
-}
-
-export const OrganizationSwitcher: React.FC = ({
- organizations,
- selectedOrg,
- onOrgSelect,
- isLoading = false,
-}) => {
- const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
- const isCurrentUserAdmin = selectedOrg?.userPermission?.accessType === 'ADMIN';
-
- if (isLoading || !selectedOrg) {
- return ;
- }
-
- const handleOrgSelect = async (org: Organization) => {
- if (org.slug === selectedOrg?.slug) return;
- await onOrgSelect(org);
- };
-
- const trigger = (
-
-
-
-
- {selectedOrg.name}
-
-
-
-
- );
-
- return (
-
-
-
- {organizations.map((org) => {
- const isSelected = org.slug === selectedOrg?.slug;
- return (
- handleOrgSelect(org)}
- className={isSelected ? 'bg-gray-50 cursor-default' : ''}
- >
-
-
-
- {org.name}
-
- {isSelected &&
}
-
-
- );
- })}
-
-
-
-
-
setIsSettingsModalOpen(true)}
- disabled={!isCurrentUserAdmin}
- tooltip={isCurrentUserAdmin ? undefined : 'Only admins can manage organization settings'}
- >
-
- {isCurrentUserAdmin ? (
-
- ) : (
-
- )}
-
- Manage
-
-
-
- {selectedOrg.memberCount} {selectedOrg.memberCount === 1 ? 'member' : 'members'}
-
-
-
-
- {selectedOrg && isCurrentUserAdmin && (
-
setIsSettingsModalOpen(false)}
- />
- )}
-
- );
-};
diff --git a/components/Notebook/LeftSidebar/SidebarSection.tsx b/components/Notebook/LeftSidebar/SidebarSection.tsx
deleted file mode 100644
index 1e36e277e..000000000
--- a/components/Notebook/LeftSidebar/SidebarSection.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import React, { useState } from 'react';
-import type { LucideIcon } from 'lucide-react';
-import { Button } from '@/components/ui/Button';
-
-interface SidebarSectionProps {
- title: React.ReactNode;
- children?: React.ReactNode;
- action?: React.ReactNode;
- icon?: LucideIcon;
- iconPosition?: 'before' | 'after';
- defaultExpanded?: boolean;
- /** Render a full-width divider beneath the section title. */
- divider?: boolean;
-}
-
-export const SidebarSection: React.FC = ({
- title,
- children,
- action,
- icon: Icon,
- iconPosition = 'before',
- defaultExpanded = true,
- divider = false,
-}) => {
- const [isExpanded, setIsExpanded] = useState(defaultExpanded);
-
- return (
-
-
-
setIsExpanded(!isExpanded)}
- >
- {Icon && iconPosition === 'before' && }
-
- {title}
-
- {Icon && iconPosition === 'after' && }
-
- {action &&
{action}
}
-
- {/* Full-bleed divider: -mx-2 cancels the notebook sidebar's px-2 wrapper. */}
- {divider &&
}
- {isExpanded && children &&
{children}
}
-
- );
-};
diff --git a/components/Notebook/NoteStatus.tsx b/components/Notebook/NoteStatus.tsx
deleted file mode 100644
index 27cdbac70..000000000
--- a/components/Notebook/NoteStatus.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { cn } from '@/utils/styles';
-
-/**
- * Whether a note is out in the world or still being written, as a dot: blue
- * once published, amber while a draft. The same two colours wherever notes
- * are listed — the workspace sidebar, the My Funding sections.
- */
-const NOTE_STATUS = {
- published: { label: 'Published', dotClass: 'bg-primary-500' },
- draft: { label: 'Draft', dotClass: 'bg-amber-400' },
-} as const;
-
-interface NoteStatusDotProps {
- readonly published: boolean;
- /** The status is written out beside the dot, so the dot itself says nothing. */
- readonly decorative?: boolean;
- readonly className?: string;
-}
-
-export function NoteStatusDot({ published, decorative = false, className }: NoteStatusDotProps) {
- const status = published ? NOTE_STATUS.published : NOTE_STATUS.draft;
- const classes = cn('inline-block h-2 w-2 shrink-0 rounded-full', status.dotClass, className);
- if (decorative) return ;
- return ;
-}
-
-interface NoteStatusLineProps extends NoteStatusDotProps {
- /** After the status, e.g. when it was last edited. */
- readonly detail?: string;
-}
-
-/**
- * "● Draft · Edited 2 hours ago": over a published card, or inside a draft's
- * row. A span, so it can sit inside a button as well as stand on its own.
- */
-export function NoteStatusLine({ published, detail, className }: NoteStatusLineProps) {
- const status = published ? NOTE_STATUS.published : NOTE_STATUS.draft;
- return (
-
-
- {status.label}
- {detail && (
- <>
- ·
- {detail}
- >
- )}
-
- );
-}
diff --git a/components/Notebook/PublishingForm/components/FundingGoalSection.tsx b/components/Notebook/PublishingForm/components/FundingGoalSection.tsx
deleted file mode 100644
index b1c0146fe..000000000
--- a/components/Notebook/PublishingForm/components/FundingGoalSection.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { DollarSign } from 'lucide-react';
-import { cn } from '@/utils/styles';
-import { FundingGoalField } from './FundingGoalField';
-import { SectionHeader } from './SectionHeader';
-import type { SectionProps } from './SectionProps';
-
-/** The proposal's funding goal on its own, headed like the other sections. */
-export function FundingGoalSection({ className }: SectionProps) {
- return (
-
- Funding Goal
-
-
- );
-}
diff --git a/components/Notebook/PublishingForm/components/GrantApplicationDeadlineSection.tsx b/components/Notebook/PublishingForm/components/GrantApplicationDeadlineSection.tsx
deleted file mode 100644
index 557af833a..000000000
--- a/components/Notebook/PublishingForm/components/GrantApplicationDeadlineSection.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { useFormContext } from 'react-hook-form';
-import { Calendar } from 'lucide-react';
-import { DatePicker } from '@/components/ui/form/DatePicker';
-import { SectionHeader } from './SectionHeader';
-import { PublishingFormData } from '../schema';
-
-export function GrantApplicationDeadlineSection() {
- const {
- watch,
- setValue,
- formState: { errors },
- } = useFormContext();
-
- const applicationDeadline = watch('applicationDeadline');
-
- return (
-
-
Application Deadline
-
- setValue('applicationDeadline', date, { shouldValidate: true })}
- placeholder="Select deadline date"
- error={errors.applicationDeadline?.message?.toString()}
- helperText="When applications for this RFP should be submitted by"
- minDate={new Date()} // Prevent selecting past dates
- required
- />
-
-
- );
-}
diff --git a/components/banners/EarningOpportunityBanner.tsx b/components/banners/EarningOpportunityBanner.tsx
deleted file mode 100644
index 8345250b9..000000000
--- a/components/banners/EarningOpportunityBanner.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
-import { useExchangeRate } from '@/contexts/ExchangeRateContext';
-import { CurrencyBadge } from '@/components/ui/CurrencyBadge';
-import { Button } from '@/components/ui/Button';
-import { getActiveBounties, getTotalBountyDisplayAmount } from '@/components/Bounty/lib/bountyUtil';
-import { buildWorkUrl } from '@/utils/url';
-import { useRouter } from 'next/navigation';
-import { Work } from '@/types/work';
-import { WorkMetadata } from '@/services/metadata.service';
-import { DollarSign } from 'lucide-react';
-
-interface EarningOpportunityBannerProps {
- work: Work;
- metadata: WorkMetadata;
- onViewBounties?: () => void;
-}
-
-export const EarningOpportunityBanner = ({
- work,
- metadata,
- onViewBounties,
-}: EarningOpportunityBannerProps) => {
- const { showUSD } = useCurrencyPreference();
- const { exchangeRate, isLoading: isExchangeRateLoading } = useExchangeRate();
- const router = useRouter();
-
- // Don't show banner if no active bounties
- if (!metadata.bounties || metadata.activeBounties === 0) {
- return null;
- }
-
- // Calculate display amount (handles Foundation bounties with flat $150 USD)
- const activeBounties = useMemo(
- () => getActiveBounties(metadata.bounties || []),
- [metadata.bounties]
- );
- const { amount: displayAmount } = useMemo(
- () => getTotalBountyDisplayAmount(activeBounties, exchangeRate, showUSD),
- [activeBounties, exchangeRate, showUSD]
- );
-
- // Check if we can display the bounty amount (exchange rate loaded if USD preferred)
- const canDisplayAmount = !showUSD || (showUSD && !isExchangeRateLoading && exchangeRate > 0);
-
- const handleViewBounties = () => {
- if (onViewBounties) {
- onViewBounties();
- } else {
- const bountiesUrl = buildWorkUrl({
- id: work.id,
- contentType: work.contentType === 'paper' ? 'paper' : 'post',
- slug: work.slug,
- tab: 'bounties',
- });
- router.push(bountiesUrl);
- }
- };
-
- return (
-
-
- {/* Mobile layout - compact single row */}
-
-
-
- Earn
- {canDisplayAmount && (
- <>
- {showUSD && (
-
- )}
-
- >
- )}
-
- Peer Review
-
-
-
-
{
- e.stopPropagation();
- handleViewBounties();
- }}
- size="sm"
- className="flex-shrink-0 bg-orange-500 hover:bg-orange-600 text-white font-medium shadow-sm group-hover:shadow transition-all duration-200 text-xs px-3"
- aria-label={`View ${metadata.activeBounties} available ${metadata.activeBounties === 1 ? 'bounty' : 'bounties'}`}
- >
- View
-
-
-
- {/* Tablet+ layout - stacked with full-width button */}
-
-
-
- Earn
- {canDisplayAmount && (
- <>
- {showUSD && (
-
- )}
-
- >
- )}
-
-
- Peer Review
-
-
-
- Submit a peer review to earn ResearchCoin.
-
-
{
- e.stopPropagation();
- handleViewBounties();
- }}
- size="sm"
- className="w-full mt-3 bg-orange-500 hover:bg-orange-600 text-white font-medium shadow-sm group-hover:shadow transition-all duration-200"
- aria-label={`View ${metadata.activeBounties} available ${metadata.activeBounties === 1 ? 'bounty' : 'bounties'}`}
- >
- View Bounties
-
-
-
-
- );
-};
diff --git a/components/banners/HaveYouPublishedBanner.tsx b/components/banners/HaveYouPublishedBanner.tsx
deleted file mode 100644
index 9b08b6ebe..000000000
--- a/components/banners/HaveYouPublishedBanner.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-'use client';
-
-import { Settings, User, X } from 'lucide-react';
-import { useState } from 'react';
-import { Button } from '@/components/ui/Button';
-import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon';
-
-export const HaveYouPublishedBanner = () => {
- const [isVisible, setIsVisible] = useState(true);
-
- if (!isVisible) return null;
-
- return (
-
-
setIsVisible(false)}
- className="absolute top-3 right-3 text-gray-400 hover:text-gray-600 transition-colors"
- >
-
-
-
Is this your paper?
-
-
-
- Earn rewards for your paper
-
-
-
- Be able to customize this page
-
-
-
- Appear as author in comment section
-
-
-
- Claim your paper
-
-
- );
-};
diff --git a/components/charts/BarChart.tsx b/components/charts/BarChart.tsx
deleted file mode 100644
index bdad03a6b..000000000
--- a/components/charts/BarChart.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-'use client';
-
-import { Bar } from 'react-chartjs-2';
-import {
- Chart as ChartJS,
- CategoryScale,
- LinearScale,
- BarElement,
- Title,
- Tooltip,
- Legend,
- ChartData,
-} from 'chart.js';
-
-ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
-
-const options = {
- responsive: true,
- plugins: {
- legend: {
- display: false,
- },
- },
- scales: {
- y: {
- beginAtZero: true,
- },
- },
-};
-
-interface BarChartProps {
- data: ChartData<'bar'>;
-}
-
-export function BarChart({ data }: BarChartProps) {
- return ;
-}
diff --git a/components/charts/YieldChart.tsx b/components/charts/YieldChart.tsx
deleted file mode 100644
index 391c775dc..000000000
--- a/components/charts/YieldChart.tsx
+++ /dev/null
@@ -1,321 +0,0 @@
-'use client';
-
-import { Line } from 'react-chartjs-2';
-import {
- Chart as ChartJS,
- CategoryScale,
- LinearScale,
- PointElement,
- LineElement,
- Title,
- Tooltip,
- Filler,
-} from 'chart.js';
-import { StakingYieldRange } from '@/services/staking-yield.service';
-import { useStakingYieldHistory, useStakingYieldStats } from '@/hooks/useStakingYield';
-import { formatRSC } from '@/utils/number';
-
-ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Filler);
-
-const RANGE_OPTIONS: { value: StakingYieldRange; label: string }[] = [
- { value: '7d', label: '7D' },
- { value: '30d', label: '30D' },
- { value: '90d', label: '90D' },
- { value: 'all', label: 'All' },
-];
-
-interface YieldChartProps {
- compact?: boolean;
- height?: number;
- defaultRange?: StakingYieldRange;
- showRangeSelector?: boolean;
- title?: string;
- /** Show numeric axis labels + grid. Defaults to true. */
- showAxes?: boolean;
- /** Use a top-to-bottom gradient fill instead of a flat opacity tint. */
- gradientFill?: boolean;
- /** Show summary stats (yield, total staked, tokens issued today) above the chart. */
- showStats?: boolean;
-}
-
-export function YieldChart({
- compact = false,
- height,
- defaultRange = '90d',
- showRangeSelector = true,
- title = 'Annualized Yield',
- showAxes = true,
- gradientFill = false,
- showStats = false,
-}: Readonly) {
- const { history, isLoading, range, setRange } = useStakingYieldHistory(defaultRange);
- const { stats, isLoading: isStatsLoading } = useStakingYieldStats();
- const chartHeight = height ?? (compact ? 150 : 300);
-
- const labels =
- history?.results.map((r) =>
- new Date(r.accrual_date + 'T00:00:00').toLocaleDateString('en-US', {
- month: 'short',
- day: 'numeric',
- })
- ) ?? [];
-
- const apyData = history?.results.map((r) => r.apy) ?? [];
- const tvlData =
- history?.results.map((r) =>
- r.total_value_locked_usd ? parseFloat(r.total_value_locked_usd) : null
- ) ?? [];
- const latestApy = stats?.apy ?? (apyData.length ? apyData[apyData.length - 1] : null);
-
- const totalStakedRsc = stats ? parseFloat(stats.total_staked_rsc) : null;
- const issuedTodayRsc = stats?.issued_today_rsc ? parseFloat(stats.issued_today_rsc) : null;
-
- const yieldColor = '#3971ff';
- const tvlColor = '#10b981';
- const yieldFill = gradientFill
- ? (context: any) => {
- const { ctx, chartArea } = context.chart;
- if (!chartArea) return undefined;
- const gradient = ctx.createLinearGradient(0, chartArea.top, 0, chartArea.bottom);
- gradient.addColorStop(0, 'rgba(57, 113, 255, 0.28)');
- gradient.addColorStop(1, 'rgba(57, 113, 255, 0)');
- return gradient;
- }
- : 'rgba(57, 113, 255, 0.08)';
-
- const hasTvl = tvlData.some((v) => v != null);
-
- const chartData = {
- labels,
- datasets: [
- {
- label: 'Yield',
- data: apyData,
- borderColor: yieldColor,
- backgroundColor: yieldFill,
- fill: true,
- tension: 0.3,
- pointRadius: 0,
- pointHoverRadius: compact ? 2 : 4,
- borderWidth: compact ? 1.5 : 2,
- yAxisID: 'y',
- },
- ...(hasTvl
- ? [
- {
- label: 'TVL',
- data: tvlData,
- borderColor: tvlColor,
- backgroundColor: 'transparent',
- fill: false,
- tension: 0.3,
- pointRadius: 0,
- pointHoverRadius: compact ? 2 : 4,
- borderWidth: compact ? 1.5 : 2,
- borderDash: [4, 3],
- yAxisID: 'y1',
- },
- ]
- : []),
- ],
- };
-
- const options = {
- responsive: true,
- maintainAspectRatio: false,
- interaction: {
- intersect: false,
- mode: 'index' as const,
- },
- plugins: {
- legend: { display: false },
- tooltip: {
- usePointStyle: true,
- callbacks: {
- label: (ctx: any) => {
- if (ctx.dataset.yAxisID === 'y1') {
- const val = ctx.parsed.y;
- if (val >= 1_000_000) return `TVL: $${(val / 1_000_000).toFixed(2)}m`;
- if (val >= 1_000) return `TVL: $${(val / 1_000).toFixed(1)}k`;
- return `TVL: $${val.toFixed(2)}`;
- }
- return `Yield: ${ctx.parsed.y.toFixed(2)}%`;
- },
- },
- },
- },
- scales: showAxes
- ? {
- x: {
- grid: { display: false },
- ticks: {
- maxTicksLimit: compact ? 4 : 8,
- color: '#9ca3af',
- font: { size: compact ? 10 : 11 },
- },
- },
- y: {
- position: 'left' as const,
- beginAtZero: true,
- grid: { color: '#f3f4f6' },
- ticks: {
- callback: (value: any) => `${value}%`,
- color: '#9ca3af',
- font: { size: compact ? 10 : 11 },
- },
- },
- ...(hasTvl
- ? {
- y1: {
- position: 'right' as const,
- beginAtZero: true,
- grid: { display: false },
- ticks: {
- callback: (value: any) => {
- if (value >= 1_000_000) return `$${(value / 1_000_000).toFixed(1)}m`;
- if (value >= 1_000) return `$${(value / 1_000).toFixed(0)}k`;
- return `$${value}`;
- },
- color: '#9ca3af',
- font: { size: compact ? 10 : 11 },
- },
- },
- }
- : {}),
- }
- : {
- x: { display: false, grid: { display: false } },
- y: { display: false, beginAtZero: true, grid: { display: false } },
- ...(hasTvl
- ? { y1: { display: false, beginAtZero: true, grid: { display: false } } }
- : {}),
- },
- };
-
- return (
-
- {(!compact || title) && (
-
- {title && (
-
- {title}
-
- )}
- {showRangeSelector && (
-
- {RANGE_OPTIONS.map((opt) => (
- setRange(opt.value)}
- className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
- range === opt.value
- ? 'bg-primary-500 text-white'
- : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
- }`}
- >
- {opt.label}
-
- ))}
-
- )}
-
- )}
-
- {!compact && hasTvl && (
-
-
-
- Yield
-
-
-
- TVL
-
-
- )}
-
- {showStats && (
-
-
-
-
-
- )}
-
-
- {isLoading && (
-
- )}
- {!isLoading && history?.results.length === 0 && (
-
- No data available for this period
-
- )}
- {!isLoading && history && history.results.length > 0 && (
-
- )}
-
-
- );
-}
-
-function StatLine({
- label,
- value,
- isLoading,
-}: {
- label: string;
- value: string | null;
- isLoading: boolean;
-}) {
- return (
-
-
{label}
- {isLoading ? (
-
- ) : (
-
{value ?? '—'}
- )}
-
- );
-}
diff --git a/components/modals/FundResearchModal.tsx b/components/modals/FundResearchModal.tsx
deleted file mode 100644
index ee1f455e6..000000000
--- a/components/modals/FundResearchModal.tsx
+++ /dev/null
@@ -1,472 +0,0 @@
-'use client';
-
-import { Dialog, Transition } from '@headlessui/react';
-import { Fragment, useEffect, useState } from 'react';
-import { Button } from '@/components/ui/Button';
-import { Input } from '@/components/ui/form/Input';
-import Image from 'next/image';
-import { ArrowLeft, ArrowDownToLine, CreditCard, ChevronDown } from 'lucide-react';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faHexagonImage } from '@fortawesome/pro-solid-svg-icons';
-import { Alert } from '@/components/ui/Alert';
-import { Tooltip } from '@/components/ui/Tooltip';
-import { cn } from '@/utils/styles';
-import { useCreateContribution } from '@/hooks/useFundraise';
-import { useSession } from 'next-auth/react';
-import { BalanceInfo } from './BalanceInfo';
-import { ID } from '@/types/root';
-import { useUser } from '@/contexts/UserContext';
-import { getAvailableAndPromotionalRscBalance } from '@/components/ResearchCoin/lib/promotionalBalance';
-interface FundResearchModalProps {
- isOpen: boolean;
- onClose: () => void;
- title: string;
- nftRewardsEnabled?: boolean;
- nftImageSrc?: string;
- fundraiseId: ID;
-}
-
-type Currency = 'RSC' | 'USD';
-type Step = 'amount' | 'payment';
-
-const ModalHeader = ({
- title,
- onClose,
- onBack,
-}: {
- title: string;
- onClose: () => void;
- onBack?: () => void;
-}) => (
-
-
-
- {onBack && (
-
-
-
- )}
-
- {title}
-
-
-
- Close
-
-
-
-
-
-
-);
-
-// Amount Step Components
-const CurrencyInput = ({
- value,
- onChange,
- currency,
- onCurrencyToggle,
- convertedAmount,
-}: {
- value: string;
- onChange: (e: React.ChangeEvent) => void;
- currency: Currency;
- onCurrencyToggle: () => void;
- convertedAmount?: string;
-}) => (
-
-
-
{currency}
-
-
-
-
- }
- />
- {convertedAmount &&
{convertedAmount}
}
-
-);
-
-// Payment Step Components
-const PaymentOption = ({
- icon,
- title,
- subtitle,
-}: {
- icon: React.ReactNode;
- title: string;
- subtitle?: string;
-}) => (
-
- {icon}
- {title}
- {subtitle && {subtitle} }
-
-);
-
-const PaymentIcons = {
- card: ,
- deposit: ,
-};
-
-const FeeBreakdown = ({
- totalAmount,
- platformFee,
- daoFee,
- incFee,
- baseAmount,
- nftCount,
- isFeesExpanded,
- onToggleExpand,
-}: {
- totalAmount: number;
- platformFee: number;
- daoFee: number;
- incFee: number;
- baseAmount: number;
- nftCount: number;
- isFeesExpanded: boolean;
- onToggleExpand: () => void;
-}) => (
-
-
- Your contribution:
- {totalAmount.toLocaleString()} RSC
-
-
-
-
-
- {platformFee.toLocaleString()} RSC
-
-
- {isFeesExpanded && (
-
-
- ResearchHub DAO (2%)
- {daoFee.toLocaleString()} RSC
-
-
- ResearchHub Inc (7%)
- {incFee.toLocaleString()} RSC
-
-
- )}
-
-
-
-
-
- Net research funding:
- {baseAmount.toLocaleString()} RSC
-
-
-
- NFTs received:
- {nftCount.toLocaleString()}
-
-
-);
-const NFTPreview = ({ rscAmount, nftCount }: { rscAmount: number; nftCount: number }) => (
-
-
- Your contribution:
- {rscAmount.toLocaleString()} RSC
-
-
- NFTs will receive:
- {nftCount.toLocaleString()}
-
-
-);
-
-export function FundResearchModal({
- isOpen,
- onClose,
- title,
- nftRewardsEnabled = false,
- nftImageSrc,
- fundraiseId,
-}: FundResearchModalProps) {
- const { user } = useUser();
- const userBalance = getAvailableAndPromotionalRscBalance(user);
- const [step, setStep] = useState('amount');
- const [inputAmount, setInputAmount] = useState(0);
- const [currency, setCurrency] = useState('RSC');
- const [isFeesExpanded, setIsFeesExpanded] = useState(false);
- const RSC_TO_USD = 1;
- const NFT_THRESHOLD_USD = 1000;
-
- const [
- { data: contributionData, isLoading: isContributing, error: contributionError },
- createContribution,
- ] = useCreateContribution();
-
- // Utility functions
- const handleAmountChange = (e: React.ChangeEvent) => {
- // Remove any non-numeric characters except decimal point
- const rawValue = e.target.value.replace(/[^0-9.]/g, '');
- const numValue = parseFloat(rawValue);
-
- if (!isNaN(numValue)) {
- setInputAmount(numValue);
- } else {
- setInputAmount(0);
- }
- };
-
- const getFormattedInputValue = () => {
- if (inputAmount === 0) return '';
- return inputAmount.toLocaleString();
- };
-
- const toggleCurrency = () => {
- setCurrency(currency === 'RSC' ? 'USD' : 'RSC');
- };
-
- const getConvertedAmount = () => {
- if (inputAmount === 0) return '';
- return currency === 'RSC'
- ? `≈ $${(inputAmount * RSC_TO_USD).toLocaleString()} USD`
- : `≈ ${(inputAmount / RSC_TO_USD).toLocaleString()} RSC`;
- };
-
- const getRscAmount = (): number => {
- return currency === 'RSC' ? inputAmount : inputAmount / RSC_TO_USD;
- };
-
- const getNFTCount = () => {
- const amountUSD = getRscAmount() * RSC_TO_USD;
- return Math.floor(amountUSD / NFT_THRESHOLD_USD);
- };
-
- const handleCreateContribution = async () => {
- try {
- await createContribution(fundraiseId, {
- amount: getRscAmount(),
- amount_currency: currency,
- });
-
- onClose();
- } catch (error) {
- // Error is handled by the hook
- console.error('Contribution failed:', error);
- }
- };
-
- // Step rendering functions
- const renderAmountStep = () => (
-
-
-
-
{title}
-
- {nftRewardsEnabled && nftImageSrc && (
-
- )}
-
-
-
- {nftRewardsEnabled ? (
-
- Contributions of $500 or more will receive a limited edition acknowledgement NFT.
-
- ) : (
-
- Your contribution will directly support groundbreaking research and advance open
- science.
-
- )}
-
-
-
-
-
- {inputAmount > 1 && nftRewardsEnabled && (
-
- )}
-
-
-
setStep('payment')}
- >
- Continue
-
-
-
- );
-
- const renderPaymentStep = () => {
- const rscAmount = getRscAmount();
- const insufficientBalance = userBalance < rscAmount;
-
- const platformFee = Math.floor(rscAmount * 0.09);
- const daoFee = Math.floor(rscAmount * 0.02);
- const incFee = Math.floor(rscAmount * 0.07);
- const baseAmount = rscAmount - platformFee;
- const nftCount = nftRewardsEnabled ? Math.floor(rscAmount / NFT_THRESHOLD_USD) : 0;
-
- return (
-
-
setStep('amount')} />
-
- setIsFeesExpanded(!isFeesExpanded)}
- />
-
-
-
-
- {nftRewardsEnabled && nftCount > 0 && (
-
- You will receive {nftCount} NFT{nftCount !== 1 ? 's' : ''} for this contribution
-
- )}
-
-
-
-
-
- {contributionError && (
-
- {contributionError}
-
- )}
-
-
- {isContributing ? 'Processing...' : 'Confirm Payment'}
-
-
- );
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
- {step === 'amount' ? renderAmountStep() : renderPaymentStep()}
-
-
-
-
-
-
- );
-}
diff --git a/components/modals/FundingRequestModal.tsx b/components/modals/FundingRequestModal.tsx
deleted file mode 100644
index d6cff067b..000000000
--- a/components/modals/FundingRequestModal.tsx
+++ /dev/null
@@ -1,269 +0,0 @@
-'use client';
-
-import { Dialog, Transition } from '@headlessui/react';
-import { Fragment, useState, ChangeEvent, FormEvent } from 'react';
-import { X } from 'lucide-react';
-import { useRouter } from 'next/navigation';
-import { Button } from '@/components/ui/Button';
-import { useSession } from 'next-auth/react';
-
-interface FundingRequestModalProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-type Step = 'study' | 'funding' | 'nft';
-
-interface StudyInfo {
- title: string;
- proposal: string;
-}
-
-interface FundingInfo {
- amount: number;
- supply: number;
-}
-
-interface NFTInfo {
- image: File | null;
-}
-
-export function FundingRequestModal({ isOpen, onClose }: FundingRequestModalProps) {
- const [currentStep, setCurrentStep] = useState('study');
- const [studyInfo, setStudyInfo] = useState({ title: '', proposal: '' });
- const [fundingInfo, setFundingInfo] = useState({ amount: 0, supply: 0 });
- const [nftInfo, setNftInfo] = useState({ image: null });
- const router = useRouter();
- const { data: session } = useSession();
-
- const handleStudySubmit = (e: FormEvent) => {
- e.preventDefault();
- setCurrentStep('funding');
- };
-
- const handleFundingSubmit = (e: FormEvent) => {
- e.preventDefault();
- setCurrentStep('nft');
- };
-
- const handleNFTSubmit = async (e: FormEvent) => {
- e.preventDefault();
- // TODO: Implement NFT upload and funding request submission
- router.push('/funding/requests');
- onClose();
- };
-
- const renderStepContent = () => {
- switch (currentStep) {
- case 'study':
- return (
-
- );
-
- case 'funding':
- return (
-
-
-
Funding Information
-
Specify your funding requirements
-
-
-
-
-
- Continue to NFT Art
-
-
- );
-
- case 'nft':
- return (
-
-
-
NFT Artwork
-
Upload artwork for your funding NFTs
-
-
-
-
-
NFT Image
-
-
-
-
-
-
-
- Upload a file
- ) => {
- const file = e.target.files?.[0];
- if (file) {
- setNftInfo({ image: file });
- }
- }}
- required
- />
-
-
or drag and drop
-
-
PNG, JPG, GIF up to 10MB
-
-
-
-
-
-
- Submit Funding Request
-
-
- );
- }
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {renderStepContent()}
-
-
-
-
-
-
- );
-}
diff --git a/components/modals/GrantDetailsModal.tsx b/components/modals/GrantDetailsModal.tsx
deleted file mode 100644
index 239bfd201..000000000
--- a/components/modals/GrantDetailsModal.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-'use client';
-
-import { ArrowUpFromLine } from 'lucide-react';
-import { BaseModal } from '@/components/ui/BaseModal';
-import { Button } from '@/components/ui/Button';
-import { PostBlockEditor } from '@/components/work/PostBlockEditor';
-
-interface GrantDetailsModalProps {
- isOpen: boolean;
- onClose: () => void;
- onSubmitProposal: () => void;
- content?: string;
- imageUrl?: string;
- isActive?: boolean;
-}
-
-export const GrantDetailsModal: React.FC = ({
- isOpen,
- onClose,
- onSubmitProposal,
- content,
- imageUrl,
- isActive = true,
-}) => {
- const footerContent = isActive ? (
- {
- onClose();
- onSubmitProposal();
- }}
- className="w-full gap-2"
- >
- Submit Proposal
-
-
- ) : null;
-
- return (
-
- {content && (
-
- )}
-
- );
-};
diff --git a/components/modals/GrantModal.tsx b/components/modals/GrantModal.tsx
deleted file mode 100644
index 8bae1aed1..000000000
--- a/components/modals/GrantModal.tsx
+++ /dev/null
@@ -1,163 +0,0 @@
-'use client';
-
-import { Dialog, Transition } from '@headlessui/react';
-import { Fragment, useState, ChangeEvent, FormEvent } from 'react';
-import { X } from 'lucide-react';
-import { useRouter } from 'next/navigation';
-import { Button } from '@/components/ui/Button';
-import { useSession } from 'next-auth/react';
-
-interface GrantModalProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-interface GrantInfo {
- title: string;
- description: string;
- amount: number;
-}
-
-export function GrantModal({ isOpen, onClose }: GrantModalProps) {
- const [grantInfo, setGrantInfo] = useState({
- title: '',
- description: '',
- amount: 10000, // Minimum 10,000 RSC
- });
- const router = useRouter();
- const { data: session } = useSession();
-
- const handleSubmit = async (e: FormEvent) => {
- e.preventDefault();
- // TODO: Implement grant submission
- router.push('/grants');
- onClose();
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Publish an RFP
-
- Fund promising research by publishing a Request for Proposals (RFP)
-
-
-
-
-
-
- Title
-
- ) =>
- setGrantInfo({ ...grantInfo, title: e.target.value })
- }
- placeholder="Enter RFP title"
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
- required
- />
-
-
-
-
- Description
-
- ) =>
- setGrantInfo({ ...grantInfo, description: e.target.value })
- }
- placeholder="Describe the research you want to fund..."
- rows={5}
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
- required
- />
-
-
-
-
- Funding Amount (RSC)
-
-
) =>
- setGrantInfo({ ...grantInfo, amount: Number(e.target.value) })
- }
- placeholder="Enter funding amount in ResearchCoin"
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
- required
- />
-
- Minimum funding amount is 10,000 RSC
-
-
-
-
-
-
RFP Process
-
- • Applications are reviewed on a rolling basis
- • Decisions are made monthly
- • Selected proposals will be announced publicly
- • Monthly progress updates are required
-
-
-
-
- Publish RFP
-
-
-
-
-
-
-
-
- );
-}
diff --git a/components/modals/KeyInsightsModal.tsx b/components/modals/KeyInsightsModal.tsx
deleted file mode 100644
index 7df13d24b..000000000
--- a/components/modals/KeyInsightsModal.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-'use client';
-
-import { FC, useMemo } from 'react';
-import { ArrowDownRight, ArrowUpRight, Sparkles } from 'lucide-react';
-import { BaseModal } from '@/components/ui/BaseModal';
-import type { KeyInsightData, KeyInsightItem } from '@/types/aiPeerReview';
-import { cn } from '@/utils/styles';
-
-interface KeyInsightsModalProps {
- isOpen: boolean;
- onClose: () => void;
- keyInsight: KeyInsightData;
-}
-
-const byOrder = (a: KeyInsightItem, b: KeyInsightItem) => a.order - b.order || a.id - b.id;
-
-export const KeyInsightsModal: FC = ({ isOpen, onClose, keyInsight }) => {
- const { allPros, allCons } = useMemo(() => {
- const pros = keyInsight.items.filter((i) => i.itemType === 'strength').sort(byOrder);
- const cons = keyInsight.items.filter((i) => i.itemType === 'weakness').sort(byOrder);
- return { allPros: pros, allCons: cons };
- }, [keyInsight]);
-
- return (
-
-
- Key Insights
-
- Beta
-
-
- }
- >
-
- {keyInsight.tldr && (
-
-
- TL;DR
-
- {keyInsight.tldr}
-
- )}
-
- {(allPros.length > 0 || allCons.length > 0) && (
-
-
- Deep Dive
-
-
- {allPros.map((i) => (
-
- ))}
- {allCons.map((i) => (
-
- ))}
-
-
- )}
-
-
- );
-};
-
-const DeepDiveItem: FC<{ item: KeyInsightItem; variant: 'pro' | 'con' }> = ({ item, variant }) => {
- const Icon = variant === 'pro' ? ArrowUpRight : ArrowDownRight;
- const iconColor = variant === 'pro' ? 'text-green-600' : 'text-red-600';
- const hasLabel = item.label.trim().length > 0;
- const showSeparateDescription = hasLabel && item.description && item.description !== item.label;
-
- return (
-
-
-
- {hasLabel &&
{item.label}
}
- {showSeparateDescription ? (
-
{item.description}
- ) : !hasLabel && item.description ? (
-
{item.description}
- ) : null}
-
-
- );
-};
diff --git a/components/modals/ResearchCoin/DafListModal.tsx b/components/modals/ResearchCoin/DafListModal.tsx
deleted file mode 100644
index c21bf94f5..000000000
--- a/components/modals/ResearchCoin/DafListModal.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-'use client';
-
-import { Wallet } from 'lucide-react';
-import { BaseModal } from '@/components/ui/BaseModal';
-import { useEndaoment } from '@/contexts/EndaomentContext';
-import type { EndaomentFund } from '@/services/endaoment.service';
-
-interface DafListModalProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-const formatUsd = (amount: number) =>
- `$${amount.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
-
-const getBalanceUsd = (fund: EndaomentFund) => parseFloat(fund.usdcBalance) || 0;
-
-export function DafListModal({ isOpen, onClose }: DafListModalProps) {
- const { funds, isFundsLoading } = useEndaoment();
-
- const total = funds.reduce((sum, f) => sum + getBalanceUsd(f), 0);
-
- return (
-
-
-
- Total balance
- {formatUsd(total)}
-
-
- {isFundsLoading ? (
-
- ) : funds.length === 0 ? (
-
- No funds found in your Endaoment account.
-
- ) : (
-
- )}
-
-
- );
-}
diff --git a/components/modals/ResearchCoin/DepositSuccessView.tsx b/components/modals/ResearchCoin/DepositSuccessView.tsx
deleted file mode 100644
index 1309e9db8..000000000
--- a/components/modals/ResearchCoin/DepositSuccessView.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { NetworkConfig } from '@/constants/tokens';
-import { TransactionSuccessView } from './shared/TransactionSuccessView';
-
-interface DepositSuccessViewProps {
- depositAmount: number;
- networkConfig: NetworkConfig;
- address: string;
-}
-
-export function DepositSuccessView({
- depositAmount,
- networkConfig,
- address,
-}: DepositSuccessViewProps) {
- return (
-
- );
-}
diff --git a/components/modals/ResearchCoin/shared/BalanceDisplay.tsx b/components/modals/ResearchCoin/shared/BalanceDisplay.tsx
deleted file mode 100644
index c79693fab..000000000
--- a/components/modals/ResearchCoin/shared/BalanceDisplay.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon';
-import { formatRSC } from '@/utils/number';
-import { cn } from '@/utils/styles';
-
-interface BalanceDisplayProps {
- currentBalance: number;
- futureBalance: number;
- futureBalanceLabel: string;
- futureBalanceColor?: 'green' | 'red' | 'gray';
- showFutureBalance?: boolean;
-}
-
-export function BalanceDisplay({
- currentBalance,
- futureBalance,
- futureBalanceLabel,
- futureBalanceColor = 'gray',
- showFutureBalance = true,
-}: BalanceDisplayProps) {
- return (
-
-
-
Current Balance
-
-
-
-
- {formatRSC({ amount: currentBalance })}
-
- RSC
-
-
-
-
- {showFutureBalance && (
- <>
-
-
-
{futureBalanceLabel}
-
-
-
-
- {formatRSC({ amount: futureBalance })}
-
- RSC
-
-
-
- >
- )}
-
- );
-}
diff --git a/components/modals/ResearchCoin/shared/NetworkSelectorSection.tsx b/components/modals/ResearchCoin/shared/NetworkSelectorSection.tsx
deleted file mode 100644
index b87ae1ac5..000000000
--- a/components/modals/ResearchCoin/shared/NetworkSelectorSection.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { NetworkSelector } from '@/components/ui/NetworkSelector';
-import { NetworkType } from '@/constants/tokens';
-
-interface NetworkSelectorSectionProps {
- selectedNetwork: NetworkType;
- onNetworkChange: (network: NetworkType) => void;
- disabled?: boolean;
- showDescription?: boolean;
- customBadges?: Partial>;
-}
-
-export function NetworkSelectorSection({
- selectedNetwork,
- onNetworkChange,
- disabled = false,
- showDescription = true,
- customBadges,
-}: NetworkSelectorSectionProps) {
- return (
-
- Network
-
-
- );
-}
diff --git a/components/shared/NotebookToggle.tsx b/components/shared/NotebookToggle.tsx
deleted file mode 100644
index 62082e3bc..000000000
--- a/components/shared/NotebookToggle.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-'use client';
-
-import { Notebook } from 'lucide-react';
-import { useRouter } from 'next/navigation';
-import { useState } from 'react';
-import { NotebookTransition } from '../transitions/NotebookTransition';
-
-interface NotebookToggleProps {
- isNotebookView: boolean;
-}
-
-export const NotebookToggle: React.FC = ({ isNotebookView }) => {
- const router = useRouter();
- const [isTransitioning, setIsTransitioning] = useState(false);
-
- const handleToggle = () => {
- setIsTransitioning(true);
- };
-
- const handleTransitionComplete = () => {
- setIsTransitioning(false);
- router.push(isNotebookView ? '/' : '/notebook');
- };
-
- return (
- <>
-
-
-
-
-
-
- Lab Notebook
-
-
-
-
- >
- );
-};
diff --git a/components/skeletons/OrganizationSwitcherSkeleton.tsx b/components/skeletons/OrganizationSwitcherSkeleton.tsx
deleted file mode 100644
index a76704492..000000000
--- a/components/skeletons/OrganizationSwitcherSkeleton.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import React from 'react';
-
-export const OrganizationSwitcherSkeleton: React.FC = () => {
- return (
-
- {/* Organization Button */}
-
-
- {/* Avatar */}
-
- {/* Org Name */}
-
-
-
-
- {/* Manage Section */}
-
-
-
- {/* Combined Settings Icon + Manage Text */}
-
-
-
-
-
- );
-};
diff --git a/components/skeletons/ProposalCardSkeleton.tsx b/components/skeletons/ProposalCardSkeleton.tsx
deleted file mode 100644
index 950eb3a99..000000000
--- a/components/skeletons/ProposalCardSkeleton.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { cn } from '@/utils/styles';
-
-interface ProposalCardSkeletonProps {
- className?: string;
-}
-
-export function ProposalCardSkeleton({ className }: ProposalCardSkeletonProps) {
- return (
-
- );
-}
-
-export default ProposalCardSkeleton;
diff --git a/components/tooltips/DafTooltip.tsx b/components/tooltips/DafTooltip.tsx
deleted file mode 100644
index c1c18c7d3..000000000
--- a/components/tooltips/DafTooltip.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-'use client';
-
-import { ReactNode } from 'react';
-import { Tooltip } from '@/components/ui/Tooltip';
-
-interface DafTooltipProps {
- children: ReactNode;
-}
-
-export function DafTooltip({ children }: DafTooltipProps) {
- const content = (
-
-
Donor-Advised Fund
-
- Donate through a tax-advantaged DAF and route the proceeds to research. At this time, the
- only supported DAF Provider is Endaoment.
-
-
- );
-
- return (
-
- {children}
-
- );
-}
diff --git a/components/tooltips/EndaomentTooltip.tsx b/components/tooltips/EndaomentTooltip.tsx
deleted file mode 100644
index f75c96cfe..000000000
--- a/components/tooltips/EndaomentTooltip.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-'use client';
-
-import { ReactNode } from 'react';
-import Image from 'next/image';
-import { ExternalLink } from 'lucide-react';
-import { Tooltip } from '@/components/ui/Tooltip';
-
-interface EndaomentTooltipProps {
- children: ReactNode;
-}
-
-export function EndaomentTooltip({ children }: EndaomentTooltipProps) {
- const content = (
-
-
-
- Endaoment is a non-profit that powers Donor-Advised Funds. Donate funds and route the
- proceeds to research. Tax-deductible.
-
-
-
- );
-
- return (
-
- {children}
-
- );
-}
diff --git a/components/transitions/NotebookTransition.tsx b/components/transitions/NotebookTransition.tsx
deleted file mode 100644
index 508f8b107..000000000
--- a/components/transitions/NotebookTransition.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-import { FlaskConical } from 'lucide-react';
-import { createPortal } from 'react-dom';
-
-interface NotebookTransitionProps {
- isActive: boolean;
- onComplete: () => void;
- isExit?: boolean;
-}
-
-export const NotebookTransition: React.FC = ({
- isActive,
- onComplete,
- isExit = false,
-}) => {
- const [opacity, setOpacity] = useState(0);
- const [mounted, setMounted] = useState(false);
-
- useEffect(() => {
- setMounted(true);
- return () => setMounted(false);
- }, []);
-
- useEffect(() => {
- if (isActive) {
- setOpacity(1);
- const timer = setTimeout(() => {
- setOpacity(0);
- setTimeout(onComplete, 500);
- }, 1000);
- return () => clearTimeout(timer);
- }
- }, [isActive, onComplete]);
-
- if (!isActive || !mounted) return null;
-
- const transitionContent = (
-
- );
-
- return createPortal(transitionContent, document.body);
-};
diff --git a/components/ui/NetworkSelector.tsx b/components/ui/NetworkSelector.tsx
deleted file mode 100644
index 8dd38db8e..000000000
--- a/components/ui/NetworkSelector.tsx
+++ /dev/null
@@ -1,128 +0,0 @@
-'use client';
-
-import { useMemo } from 'react';
-import Image from 'next/image';
-import { Check, ChevronDown } from 'lucide-react';
-import { BaseMenu, BaseMenuItem } from './form/BaseMenu';
-import { NETWORK_CONFIG, NetworkType } from '@/constants/tokens';
-import { cn } from '@/utils/styles';
-
-interface NetworkSelectorProps {
- value: NetworkType;
- onChange: (network: NetworkType) => void;
- disabled?: boolean;
- className?: string;
- showBadges?: boolean;
- customBadges?: Partial>;
- showDescription?: boolean;
-}
-
-export function NetworkSelector({
- value,
- onChange,
- disabled = false,
- className,
- showBadges = true,
- customBadges,
- showDescription = true,
-}: NetworkSelectorProps) {
- const selectedNetwork = NETWORK_CONFIG[value];
-
- const getBadge = (network: NetworkType): string | undefined => {
- if (customBadges?.[network]) {
- return customBadges[network];
- }
- return NETWORK_CONFIG[network].badge;
- };
-
- const trigger = useMemo(
- () => (
-
-
-
-
- {selectedNetwork.name}
- {showBadges && value === 'BASE' && getBadge(value) && (
-
- {getBadge(value)}
-
- )}
-
- {showDescription && showBadges && selectedNetwork.description && (
-
{selectedNetwork.description}
- )}
-
-
-
- ),
- [selectedNetwork, value, disabled, className, showBadges, showDescription, customBadges]
- );
-
- return (
-
- {(Object.keys(NETWORK_CONFIG) as NetworkType[]).map((network) => {
- const config = NETWORK_CONFIG[network];
- const isSelected = network === value;
-
- return (
- {
- if (!disabled) {
- onChange(network);
- }
- }}
- className={cn(
- 'flex items-center gap-3 px-3 py-3 rounded-md',
- isSelected && 'bg-primary-50'
- )}
- >
-
-
-
- {config.name}
- {showBadges && network === 'BASE' && getBadge(network) && (
-
- {getBadge(network)}
-
- )}
-
- {showDescription && showBadges && config.description && (
-
- {config.description}
-
- )}
-
- {isSelected && }
-
- );
- })}
-
- );
-}
diff --git a/components/ui/ProgressUpdates.tsx b/components/ui/ProgressUpdates.tsx
deleted file mode 100644
index f3a5b8f7d..000000000
--- a/components/ui/ProgressUpdates.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-import React from 'react';
-
-interface Update {
- id: number;
- createdDate: string;
- content?: any;
-}
-
-interface ProgressUpdatesProps {
- updates: Update[];
- className?: string;
-}
-
-export const ProgressUpdates: React.FC = ({
- updates = [],
- className = '',
-}) => {
- // Generate exactly 12 months starting from current date
- const generateTimeline = () => {
- const timeline = [];
- const now = new Date();
- const startDate = new Date(now.getFullYear(), now.getMonth(), 1); // Start from current month
- const current = new Date(startDate);
- let monthCount = 0;
-
- while (monthCount < 12) {
- const monthYear = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, '0')}`;
- const monthName = current.toLocaleDateString('en-US', { month: 'short' });
- const year = current.getFullYear();
-
- const monthUpdates = updates.filter((update) => {
- const updateDate = new Date(update.createdDate);
- return (
- updateDate.getMonth() === current.getMonth() &&
- updateDate.getFullYear() === current.getFullYear()
- );
- });
-
- timeline.push({
- monthYear,
- monthName,
- year,
- hasUpdate: monthUpdates.length > 0,
- updateCount: monthUpdates.length,
- });
-
- current.setMonth(current.getMonth() + 1);
- monthCount++;
- }
-
- return timeline;
- };
-
- const timeline = generateTimeline();
-
- return (
-
- {/* Monthly Timeline */}
-
- {timeline.map((month) => {
- const now = new Date();
- const monthDate = new Date(month.year, new Date(`${month.monthName} 1, 2000`).getMonth());
- const currentMonthStart = new Date(now.getFullYear(), now.getMonth(), 1);
- const isInPast = monthDate < currentMonthStart;
- const isPastWithoutUpdates = isInPast && !month.hasUpdate;
-
- return (
-
1 ? 's' : ''}`
- : `${month.monthName} ${month.year} - No updates`
- }
- >
- {/* Diagonal lines for past months without updates */}
- {isPastWithoutUpdates && (
-
- )}
-
-
{month.monthName}
-
- {month.year.toString().slice(-2)}
-
-
- {/* Update Count Badge - Top Right Corner */}
- {month.hasUpdate && (
-
- {month.updateCount}
-
- )}
-
- );
- })}
-
-
- {/* Compact Legend */}
-
-
- );
-};
diff --git a/components/ui/StatusCard.tsx b/components/ui/StatusCard.tsx
deleted file mode 100644
index fca34a95f..000000000
--- a/components/ui/StatusCard.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-'use client';
-
-import { FC, ReactNode } from 'react';
-import { cn } from '@/utils/styles';
-
-interface StatusCardProps {
- variant?: 'active' | 'inactive' | 'orange';
- /** Additional className for the outer container */
- className?: string;
- /** Content to render inside the card */
- children: ReactNode;
-}
-
-/**
- * StatusCard component that provides consistent styling for active/inactive states
- * Used in FundraiseProgress, GrantInfo, and BountyInfo components
- */
-export const StatusCard: FC = ({ variant = 'active', className, children }) => {
- const getVariantStyles = () => {
- switch (variant) {
- case 'active':
- return 'bg-primary-50 border-primary-100';
- case 'orange':
- return 'bg-orange-50 border-orange-100';
- case 'inactive':
- default:
- return 'bg-gray-50 border-gray-200';
- }
- };
-
- return (
-
- {children}
-
- );
-};
diff --git a/components/ui/badges/UpdateRateBadge.tsx b/components/ui/badges/UpdateRateBadge.tsx
deleted file mode 100644
index 7d7a4006e..000000000
--- a/components/ui/badges/UpdateRateBadge.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-'use client';
-
-import { Tooltip } from '@/components/ui/Tooltip';
-
-interface UpdateRateBadgeProps {
- updateRate: number;
- className?: string;
-}
-
-export const UpdateRateBadge = ({ updateRate, className = '' }: UpdateRateBadgeProps) => {
- const tooltipContent = (
-
-
Update Rate
-
- This statistic aims to inform how communicative the authors are during the course of their
- research.
-
-
- );
-
- return (
-
-
- 100% update rate
-
-
- );
-};
diff --git a/components/ui/form/CurrencyInput.tsx b/components/ui/form/CurrencyInput.tsx
deleted file mode 100644
index 12a9d2c0f..000000000
--- a/components/ui/form/CurrencyInput.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import React from 'react';
-import { Input } from './Input';
-import { ArrowRightLeft, DollarSign } from 'lucide-react';
-import { Currency } from '@/types/root';
-import { ResearchCoinIcon } from '@/components/ui/icons/ResearchCoinIcon';
-
-interface CurrencyInputProps {
- value: string | number;
- onChange: (e: React.ChangeEvent) => void;
- currency: Currency;
- onCurrencyToggle: () => void;
- convertedAmount?: string;
- suggestedAmount?: string;
- error?: string;
- isExchangeRateLoading?: boolean;
- label?: string;
- className?: string;
-}
-
-export const CurrencyInput = ({
- value,
- onChange,
- currency,
- onCurrencyToggle,
- convertedAmount,
- suggestedAmount,
- error,
- isExchangeRateLoading,
- label = 'I am offering',
- className = '',
-}: CurrencyInputProps) => {
- const currentAmount =
- typeof value === 'string' ? parseFloat(value.replace(/,/g, '')) || 0 : value;
- const suggestedAmountValue = suggestedAmount
- ? parseFloat(suggestedAmount.replace(/[^0-9.]/g, ''))
- : 0;
-
- const isBelowSuggested = currentAmount < suggestedAmountValue;
- const suggestedTextColor = !currentAmount
- ? 'text-gray-500'
- : isBelowSuggested
- ? 'text-orange-500'
- : 'text-green-500';
-
- return (
-
-
- {currency === 'RSC' ? (
-
- ) : (
-
- )}
-
{currency}
-
-
- }
- />
- {error &&
{error}
}
- {suggestedAmount && !error && (
-
- Suggested amount for peer review: {suggestedAmount}
- {currency === 'RSC' &&
- !isExchangeRateLoading &&
- !suggestedAmount.includes('Loading') &&
- ' (150 USD)'}
-
- )}
- {isExchangeRateLoading ? (
-
Loading exchange rate...
- ) : (
- convertedAmount && (
-
- {convertedAmount}
-
- )
- )}
-
- );
-};
diff --git a/components/work/KeyInsights/KeyInsightsLine.tsx b/components/work/KeyInsights/KeyInsightsLine.tsx
deleted file mode 100644
index b5bf35a2e..000000000
--- a/components/work/KeyInsights/KeyInsightsLine.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-'use client';
-
-import { FC, useMemo } from 'react';
-import { ChevronRight, Sparkles } from 'lucide-react';
-import { cn } from '@/utils/styles';
-import type { KeyInsightData } from '@/types/aiPeerReview';
-import { InsightArrow, topInsightItems } from './shared';
-
-interface KeyInsightsLineProps {
- keyInsight: KeyInsightData;
- onOpenModal: (e: React.MouseEvent) => void;
-}
-
-/**
- * Compact gradient callout — used inline below the author meta row when the
- * funder dashboard isn't wide enough for the side panel. The whole container
- * is the click target (opens the modal). Items use grid auto-fit so they
- * sit side-by-side when there's room and stack vertically when narrow.
- */
-export const KeyInsightsLine: FC = ({ keyInsight, onOpenModal }) => {
- const items = useMemo(() => topInsightItems(keyInsight), [keyInsight]);
-
- return (
-
-
-
-
- Key insights
-
-
-
-
- {items.map((item) => (
-
-
- {item.label}
-
- ))}
-
-
- );
-};
diff --git a/components/work/KeyInsights/KeyInsightsPanel.tsx b/components/work/KeyInsights/KeyInsightsPanel.tsx
deleted file mode 100644
index b06266b94..000000000
--- a/components/work/KeyInsights/KeyInsightsPanel.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-'use client';
-
-import { FC, useMemo } from 'react';
-import { Sparkles } from 'lucide-react';
-import { cn } from '@/utils/styles';
-import type { KeyInsightData } from '@/types/aiPeerReview';
-import { InsightArrow, topInsightItems } from './shared';
-
-interface KeyInsightsPanelProps {
- keyInsight: KeyInsightData;
- onOpenModal: (e: React.MouseEvent) => void;
-}
-
-/** Vertical panel — used in the funder dashboard's wide side column. */
-export const KeyInsightsPanel: FC = ({ keyInsight, onOpenModal }) => {
- const items = useMemo(() => topInsightItems(keyInsight), [keyInsight]);
-
- return (
- <>
-
-
- Key insights
-
- {items.length > 0 && (
-
- {items.map((item) => (
-
-
-
- {item.label}
-
-
- ))}
-
- )}
-
- View full insights →
-
- >
- );
-};
diff --git a/components/work/KeyInsights/shared.tsx b/components/work/KeyInsights/shared.tsx
deleted file mode 100644
index 0652c8348..000000000
--- a/components/work/KeyInsights/shared.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { FC } from 'react';
-import { ArrowDownRight, ArrowUpRight } from 'lucide-react';
-import type { KeyInsightData, KeyInsightItem } from '@/types/aiPeerReview';
-
-export const MAX_INSIGHT_ITEMS_PER_KIND = 1;
-
-const sortByOrder = (a: KeyInsightItem, b: KeyInsightItem) => a.order - b.order || a.id - b.id;
-
-export interface PreviewItem extends KeyInsightItem {
- variant: 'pro' | 'con';
-}
-
-/**
- * Pick the top strengths + weaknesses (by order) for compact preview UIs.
- * Caps each kind at {@link MAX_INSIGHT_ITEMS_PER_KIND}.
- */
-export function topInsightItems(keyInsight: KeyInsightData): PreviewItem[] {
- const pros = keyInsight.items
- .filter((i) => i.itemType === 'strength')
- .sort(sortByOrder)
- .slice(0, MAX_INSIGHT_ITEMS_PER_KIND);
- const cons = keyInsight.items
- .filter((i) => i.itemType === 'weakness')
- .sort(sortByOrder)
- .slice(0, MAX_INSIGHT_ITEMS_PER_KIND);
- return [
- ...pros.map((i) => ({ ...i, variant: 'pro' as const })),
- ...cons.map((i) => ({ ...i, variant: 'con' as const })),
- ];
-}
-
-export const InsightArrow: FC<{ variant: 'pro' | 'con'; size?: number }> = ({
- variant,
- size = 12,
-}) =>
- variant === 'pro' ? (
-
- ) : (
-
- );
diff --git a/components/work/WorkLineItems.tsx b/components/work/WorkLineItems.tsx
deleted file mode 100644
index 9118a4bfb..000000000
--- a/components/work/WorkLineItems.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-'use client';
-
-import { Work } from '@/types/work';
-import { AuthorList } from '@/components/ui/AuthorList';
-import { Contact } from '@/types/note';
-import { WorkMetadata } from '@/services/metadata.service';
-import { WorkPrimaryActions } from './WorkPrimaryActions';
-
-interface WorkLineItemsProps {
- work: Work;
- showClaimButton?: boolean;
- insightsButton?: React.ReactNode;
- metadata: WorkMetadata;
- onEditClick?: () => void;
-}
-
-export const WorkLineItems = ({
- work,
- showClaimButton = true,
- insightsButton,
- metadata,
- onEditClick,
-}: WorkLineItemsProps) => {
- return (
-
-
- {work.contentType === 'funding_request' ? (
- work.note?.post?.grant?.contacts &&
- work.note.post.grant.contacts.length > 0 && (
-
-
({
- name: contact.authorProfile?.fullName || contact.name,
- verified: contact.authorProfile?.user?.isVerified,
- profileUrl: contact.authorProfile
- ? `/author/${contact.authorProfile?.id}`
- : undefined,
- authorUrl: contact.authorProfile?.user
- ? `/author/${contact.authorProfile?.id}`
- : undefined,
- }))}
- size="sm"
- className="inline-flex items-center text-gray-600 font-medium"
- delimiterClassName="mx-2 text-gray-400"
- delimiter="•"
- showAbbreviatedInMobile={true}
- mobileExpandable={true}
- />
- {work.note.post.grant.organization && (
- @ {work.note.post.grant.organization}
- )}
-
- )
- ) : (
-
-
({
- name: authorship.authorProfile.fullName,
- verified: authorship.authorProfile.user?.isVerified,
- profileUrl: `/author/${authorship.authorProfile.id}`,
- authorUrl: authorship.authorProfile.user
- ? `/author/${authorship.authorProfile.id}`
- : undefined,
- }))}
- size="sm"
- className="inline-flex items-center text-gray-600 font-medium"
- delimiterClassName="mx-2 text-gray-400"
- delimiter="•"
- showAbbreviatedInMobile={true}
- mobileExpandable={true}
- />
-
- )}
-
- {work.publishedDate && (
-
- {`Published ` +
- new Date(work.publishedDate).toLocaleDateString('en-US', {
- month: 'long',
- day: 'numeric',
- year: 'numeric',
- })}
-
- )}
-
-
-
-
- );
-};
diff --git a/components/work/WorkPrimaryActions.tsx b/components/work/WorkPrimaryActions.tsx
deleted file mode 100644
index 0388340db..000000000
--- a/components/work/WorkPrimaryActions.tsx
+++ /dev/null
@@ -1,545 +0,0 @@
-'use client';
-
-import { useState, useCallback } from 'react';
-import {
- Flag,
- Edit,
- MoreHorizontal,
- FileUp,
- Octagon,
- Share,
- CheckCircle,
- Download,
- ArrowUp,
- ArrowDown,
- Search,
- RotateCcw,
-} from 'lucide-react';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faBookmark } from '@fortawesome/free-regular-svg-icons';
-import { faBookmark as faBookmarkSolid } from '@fortawesome/free-solid-svg-icons';
-import { Work } from '@/types/work';
-import { useAuthenticatedAction } from '@/contexts/AuthModalContext';
-import { useVote } from '@/hooks/useVote';
-import { useUserVotes } from '@/hooks/useUserVotes';
-import { useCloseFundraise, useCompleteFundraise, useReopenFundraise } from '@/hooks/useFundraise';
-import toast from 'react-hot-toast';
-import { FlagContentModal } from '@/components/modals/FlagContentModal';
-import { ConfirmModal } from '@/components/modals/ConfirmModal';
-import { ReopenFundraiseModal } from '@/components/modals/ReopenFundraiseModal';
-import { useOrganizationContext } from '@/contexts/OrganizationContext';
-import { useRouter } from 'next/navigation';
-import { TipContentModal } from '@/components/modals/TipContentModal';
-import { useUser } from '@/contexts/UserContext';
-import { WorkMetadata } from '@/services/metadata.service';
-import { useShareModalContext } from '@/contexts/ShareContext';
-import { BaseMenu, BaseMenuItem } from '@/components/ui/form/BaseMenu';
-import { AddToListModal } from '@/components/UserList/AddToListModal';
-import { useIsInList } from '@/components/UserList/lib/hooks/useIsInList';
-import { useAddToList } from '@/components/UserList/lib/UserListsContext';
-import { Button } from '@/components/ui/Button';
-import { cn } from '@/utils/styles';
-import { handleDownload } from '@/utils/download';
-
-interface WorkPrimaryActionsProps {
- work: Work;
- showClaimButton?: boolean;
- insightsButton?: React.ReactNode;
- metadata: WorkMetadata;
- onEditClick?: () => void;
- className?: string;
-}
-
-export const WorkPrimaryActions = ({
- work,
- showClaimButton = true,
- insightsButton,
- metadata,
- onEditClick,
- className,
-}: WorkPrimaryActionsProps) => {
- const [isTipModalOpen, setIsTipModalOpen] = useState(false);
- const [fundraiseAction, setFundraiseAction] = useState<'close' | 'complete' | 'reopen' | null>(
- null
- );
- const [isAddToListModalOpen, setIsAddToListModalOpen] = useState(false);
- const { executeAuthenticatedAction } = useAuthenticatedAction();
- const { vote, isVoting } = useVote({
- votableEntityId: work.id,
- feedContentType: work.contentType === 'paper' ? 'PAPER' : 'POST',
- relatedDocumentTopics: work.contentType === 'paper' ? work.topics : undefined,
- relatedDocumentId: work.id.toString(),
- relatedDocumentContentType: work.contentType,
- });
-
- const [voteCount, setVoteCount] = useState(
- work.metrics?.adjustedScore ?? work.metrics?.votes ?? 0
- );
- const [isFlagModalOpen, setIsFlagModalOpen] = useState(false);
- const router = useRouter();
- const { selectedOrg } = useOrganizationContext();
- const { user } = useUser();
- const { showShareModal } = useShareModalContext();
- const { isInList } = useIsInList(work.unifiedDocumentId);
- const { isTogglingDefaultList, handleAddToList } = useAddToList({
- unifiedDocumentId: work.unifiedDocumentId,
- isInList,
- onOpenModal: () => setIsAddToListModalOpen(true),
- });
-
- const pdfFormat = work.formats?.find((format) => format.type === 'PDF');
-
- const {
- data: userVotes,
- isLoading: isLoadingVotes,
- refresh: refreshVotes,
- } = useUserVotes({
- paperIds: work.contentType === 'paper' ? [work.id] : [],
- postIds:
- work.contentType === 'post' ||
- work.contentType === 'preregistration' ||
- work.contentType === 'funding_request'
- ? [work.id]
- : [],
- });
-
- const isUpvoted =
- work.contentType === 'paper'
- ? userVotes?.papers[work.id]?.voteType === 'upvote'
- : userVotes?.posts[work.id]?.voteType === 'upvote';
-
- const isDownvoted =
- work.contentType === 'paper'
- ? userVotes?.papers[work.id]?.voteType === 'downvote'
- : userVotes?.posts[work.id]?.voteType === 'downvote';
-
- const handleVote = useCallback(
- async (direction: 'up' | 'down') => {
- const wasUpvoted = isUpvoted;
- const wasDownvoted = isDownvoted;
-
- try {
- let newVoteType: 'UPVOTE' | 'DOWNVOTE' | 'NEUTRAL';
- let countDelta: number;
-
- if (direction === 'up') {
- if (wasUpvoted) {
- newVoteType = 'NEUTRAL';
- countDelta = -1;
- } else if (wasDownvoted) {
- newVoteType = 'UPVOTE';
- countDelta = 2;
- } else {
- newVoteType = 'UPVOTE';
- countDelta = 1;
- }
- } else {
- if (wasDownvoted) {
- newVoteType = 'NEUTRAL';
- countDelta = 1;
- } else if (wasUpvoted) {
- newVoteType = 'DOWNVOTE';
- countDelta = -2;
- } else {
- newVoteType = 'DOWNVOTE';
- countDelta = -1;
- }
- }
-
- await vote(newVoteType);
- setVoteCount((prevCount) => prevCount + countDelta);
- await refreshVotes();
- } catch (error: any) {
- if (
- error?.status === 403 ||
- (error?.response && error?.response?.status === 403) ||
- (typeof error === 'object' && error?.detail === 'Can not vote on own content')
- ) {
- toast.error('Cannot vote on your own content');
- } else {
- toast.error(
- error instanceof Error
- ? error.message
- : 'Unable to process your vote. Please try again.'
- );
- }
- }
- },
- [isUpvoted, isDownvoted, vote, refreshVotes]
- );
-
- const isModerator = !!user?.isModerator;
- const isHubEditor = !!user?.authorProfile?.isHubEditor;
-
- const isAuthor =
- user?.authorProfile != null &&
- work.authors?.some((a) => a.authorProfile.id === user.authorProfile?.id);
-
- const handleEdit = useCallback(() => {
- if (selectedOrg && work.note) {
- router.push(`/notebook/${work.note.organization.slug}/${work.note.id}`);
- } else if (
- (work.contentType === 'post' || work.contentType === 'preregistration') &&
- isAuthor &&
- onEditClick
- ) {
- onEditClick();
- } else {
- toast.error('Unable to edit');
- }
- }, [work.contentType, work.note, selectedOrg, router, isAuthor, onEditClick]);
-
- const handleTipSuccess = (amount: number) => {
- toast.success(`Successfully tipped ${amount} RSC`);
- setIsTipModalOpen(false);
- };
-
- const isGrantContact =
- user?.authorProfile != null &&
- work.contentType === 'funding_request' &&
- work.note?.post?.grant?.contacts?.some(
- (contact) => contact.authorProfile?.id === user.authorProfile?.id
- );
-
- const canEdit = (() => {
- switch (work.contentType) {
- case 'paper':
- return false;
- case 'funding_request':
- return isGrantContact || isAuthor || isModerator;
- case 'post':
- case 'preregistration':
- return isAuthor;
- default:
- return selectedOrg && work.note;
- }
- })();
-
- const handleAddVersion = useCallback(() => {
- if (!user) return;
-
- let latestPaperId = work.id;
-
- if (work.versions && work.versions.length > 0) {
- const latestFlag = work.versions.find((v) => v.isLatest);
-
- if (latestFlag) {
- latestPaperId = latestFlag.paperId;
- } else {
- const sorted = [...work.versions].sort(
- (a, b) => new Date(b.publishedDate).getTime() - new Date(a.publishedDate).getTime()
- );
- latestPaperId = sorted[0].paperId;
- }
- }
-
- router.push(`/paper/${latestPaperId}/create/version`);
- }, [work.id, work.versions, user, router]);
-
- const [{ isLoading: isClosingFundraise }, closeFundraise] = useCloseFundraise();
- const [{ isLoading: isCompletingFundraise }, completeFundraise] = useCompleteFundraise();
- const [{ isLoading: isReopeningFundraise }, reopenFundraise] = useReopenFundraise();
-
- const handleCloseFundraise = useCallback(() => {
- setFundraiseAction('close');
- }, []);
-
- const handleCompleteFundraise = useCallback(() => {
- setFundraiseAction('complete');
- }, []);
-
- const canReopenFundraise = (() => {
- const fundraise = metadata.fundraising;
- if (!fundraise) return false;
- if (fundraise.status === 'COMPLETED') return false;
- return fundraise.status === 'CLOSED' || fundraise.status === 'OPEN';
- })();
-
- const confirmReopenFundraise = useCallback(
- async (durationDays: number) => {
- if (!metadata.fundraising?.id) {
- toast.error('No fundraise found');
- return;
- }
- try {
- await reopenFundraise(metadata.fundraising.id, durationDays);
- toast.success('Fundraise reopened successfully');
- setFundraiseAction(null);
- if (typeof router.refresh === 'function') {
- router.refresh();
- } else if (typeof globalThis.window !== 'undefined') {
- globalThis.location.reload();
- }
- } catch (error: any) {
- toast.error(
- error instanceof Error ? error.message : 'Failed to reopen fundraise. Please try again.'
- );
- }
- },
- [metadata.fundraising?.id, reopenFundraise, router]
- );
-
- const confirmFundraiseAction = useCallback(async () => {
- if (!metadata.fundraising?.id) {
- toast.error('No fundraise found');
- return;
- }
-
- try {
- if (fundraiseAction === 'close') {
- await closeFundraise(metadata.fundraising.id);
- toast.success('Fundraise closed successfully');
- } else if (fundraiseAction === 'complete') {
- await completeFundraise(metadata.fundraising.id);
- toast.success('Fundraise completed successfully');
- }
-
- if (typeof router.refresh === 'function') {
- router.refresh();
- } else if (typeof window !== 'undefined') {
- window.location.reload();
- }
- } catch (error: any) {
- const action = fundraiseAction === 'close' ? 'close' : 'complete';
- toast.error(
- error instanceof Error ? error.message : `Failed to ${action} fundraise. Please try again.`
- );
- }
- }, [metadata.fundraising?.id, fundraiseAction, closeFundraise, completeFundraise, router]);
-
- const getModalConfig = () => {
- switch (fundraiseAction) {
- case 'close':
- return {
- title: 'Close fundraise & refund contributors',
- message:
- 'Are you sure you want to close this fundraise? This will immediately refund all contributions to contributors and close the fundraise permanently. No funds will be distributed to Endaoment or the researcher. This action cannot be undone.',
- confirmText: 'Close fundraise & refund contributors',
- confirmButtonClass: 'bg-red-600 hover:bg-red-700',
- isLoading: isClosingFundraise,
- };
- case 'complete':
- return {
- title: 'Complete fundraise',
- message:
- 'Are you sure you want to complete this fundraise? This will mark the fundraise as completed and distribute the funds to the researcher. This action cannot be undone.',
- confirmText: 'Complete fundraise',
- confirmButtonClass: 'bg-green-600 hover:bg-green-700',
- isLoading: isCompletingFundraise,
- };
- default:
- return {
- title: '',
- message: '',
- confirmText: '',
- confirmButtonClass: '',
- isLoading: false,
- };
- }
- };
-
- const modalConfig = getModalConfig();
-
- return (
-
-
-
-
-
executeAuthenticatedAction(() => handleVote('up'))}
- disabled={isVoting || isLoadingVotes}
- className={cn(
- 'px-3 py-2 rounded-l-lg transition-colors',
- isUpvoted ? 'text-green-600 hover:bg-green-100' : 'text-gray-600 hover:bg-gray-100',
- isVoting || isLoadingVotes ? 'cursor-not-allowed' : 'cursor-pointer'
- )}
- aria-label="Upvote"
- >
-
-
-
- {voteCount}
-
-
executeAuthenticatedAction(() => handleVote('down'))}
- disabled={isVoting || isLoadingVotes}
- className={cn(
- 'px-3 py-2 rounded-r-lg transition-colors',
- isDownvoted ? 'text-red-600 hover:bg-red-100' : 'text-gray-600 hover:bg-gray-100',
- isVoting || isLoadingVotes ? 'cursor-not-allowed' : 'cursor-pointer'
- )}
- aria-label="Downvote"
- >
-
-
-
-
- {work.unifiedDocumentId && work.postType !== 'QUESTION' && (
-
-
-
- )}
-
-
- showShareModal({
- url: window.location.href,
- docTitle: work.title,
- action: 'USER_SHARED_DOCUMENT',
- shouldShowConfetti: false,
- })
- }
- className="hidden sm:!flex items-center space-x-2 px-4 py-2 bg-gray-50 text-gray-600 rounded-lg hover:bg-gray-100"
- >
-
-
-
- {insightsButton}
-
-
-
-
- }
- >
-
- showShareModal({
- url: window.location.href,
- docTitle: work.title,
- action: 'USER_SHARED_DOCUMENT',
- shouldShowConfetti: false,
- })
- }
- >
-
- Share
-
- {canEdit && (
-
-
- Edit
-
- )}
- {isAuthor && (
- executeAuthenticatedAction(handleAddVersion)}>
-
- Upload New Version
-
- )}
- {isModerator && work.contentType === 'preregistration' && metadata.fundraising?.id && (
- <>
- executeAuthenticatedAction(handleCloseFundraise)}
- >
-
- Close fundraise & refund contributors
-
- executeAuthenticatedAction(handleCompleteFundraise)}
- >
-
- Complete fundraise
-
- {canReopenFundraise && (
- executeAuthenticatedAction(() => setFundraiseAction('reopen'))}
- >
-
- Reopen/Extend fundraise
-
- )}
- >
- )}
- {pdfFormat && (
- handleDownload(pdfFormat.url, 'document.pdf')}>
-
- Download PDF
-
- )}
- {(isModerator || isHubEditor) && work.unifiedDocumentId != null && (
-
- router.push(
- `/expert-finder/library/new?unifiedDocumentId=${work.unifiedDocumentId}`
- )
- }
- >
-
- Find experts
-
- )}
- executeAuthenticatedAction(() => setIsFlagModalOpen(true))}
- >
-
- Flag Content
-
-
-
-
-
-
setIsFlagModalOpen(false)}
- documentId={work.id.toString()}
- workType={work.contentType}
- />
-
- setIsTipModalOpen(false)}
- contentId={work.id}
- feedContentType={work.contentType === 'paper' ? 'PAPER' : 'POST'}
- onTipSuccess={handleTipSuccess}
- />
-
- {work.unifiedDocumentId && (
- setIsAddToListModalOpen(false)}
- unifiedDocumentId={work.unifiedDocumentId}
- />
- )}
-
- setFundraiseAction(null)}
- onConfirm={confirmFundraiseAction}
- title={modalConfig.title}
- message={modalConfig.message}
- confirmText={modalConfig.confirmText}
- cancelText="Cancel"
- confirmButtonClass={modalConfig.confirmButtonClass}
- cancelButtonClass="bg-white text-gray-700 border border-gray-300 hover:bg-gray-50"
- />
-
- setFundraiseAction(null)}
- onConfirm={confirmReopenFundraise}
- isLoading={isReopeningFundraise}
- />
-
- );
-};
diff --git a/components/work/components/ApplicantsSection.tsx b/components/work/components/ApplicantsSection.tsx
deleted file mode 100644
index 57a09d74e..000000000
--- a/components/work/components/ApplicantsSection.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-'use client';
-
-import { FC, useState } from 'react';
-import Link from 'next/link';
-import { Avatar } from '@/components/ui/Avatar';
-import { AuthorProfile } from '@/types/authorProfile';
-import { SidebarHeader } from '@/components/ui/SidebarHeader';
-
-interface ApplicantsSectionProps {
- applicants?: AuthorProfile[];
-}
-
-export const ApplicantsSection: FC = ({ applicants = [] }) => {
- const [isExpanded, setIsExpanded] = useState(false);
- const hasApplicants = applicants.length > 0;
- const displayLimit = 5;
- const displayedApplicants = isExpanded ? applicants : applicants.slice(0, displayLimit);
- const hasMoreApplicants = applicants.length > displayLimit;
-
- return (
-
-
-
- {hasApplicants ? (
- <>
-
- {displayedApplicants.map((applicant, index) => (
-
-
-
- {applicant.fullName}
-
-
- ))}
-
-
- {hasMoreApplicants && (
- setIsExpanded(!isExpanded)}
- className="text-sm text-gray-500 mt-3 hover:text-primary-600 hover:underline"
- >
- {isExpanded ? 'Show less' : `+${applicants.length - displayLimit} more applicants`}
-
- )}
- >
- ) : (
-
- )}
-
- );
-};
diff --git a/components/work/components/MetricsSection.tsx b/components/work/components/MetricsSection.tsx
deleted file mode 100644
index 7150c7ab2..000000000
--- a/components/work/components/MetricsSection.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { BarChart2, Eye, Star } from 'lucide-react';
-
-interface MetricsSectionProps {
- metrics: {
- reviewScore?: number;
- views?: number;
- comments?: number;
- reviews?: number;
- };
-}
-
-export const MetricsSection = ({ metrics }: MetricsSectionProps) => {
- return (
-
-
-
-
Metrics
-
-
-
-
-
- Review Score
-
-
{metrics.reviewScore || 0}
-
-
-
- );
-};
diff --git a/components/work/components/SupportersSection.tsx b/components/work/components/SupportersSection.tsx
deleted file mode 100644
index 2c3261da9..000000000
--- a/components/work/components/SupportersSection.tsx
+++ /dev/null
@@ -1,145 +0,0 @@
-'use client';
-
-import { FC, useState } from 'react';
-import { Avatar } from '@/components/ui/Avatar';
-import { Tip } from '@/types/tip';
-import { Button } from '@/components/ui/Button';
-import { Icon } from '@/components/ui/icons/Icon';
-import { TipContentModal } from '@/components/modals/TipContentModal';
-import { CurrencyBadge } from '@/components/ui/CurrencyBadge';
-import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
-import { SidebarHeader } from '@/components/ui/SidebarHeader';
-
-interface SupportersSectionProps {
- tips: Tip[];
- documentId: number;
- onTip?: () => void;
-}
-
-type ConsolidatedSupporter = {
- user: Tip['user'];
- amount: number;
-};
-
-function consolidateTipsByUser(tips: Tip[]): ConsolidatedSupporter[] {
- const supporterTotals = new Map();
-
- for (const tip of tips) {
- const existingTotal = supporterTotals.get(tip.user.id);
- supporterTotals.set(tip.user.id, {
- user: tip.user,
- amount: (existingTotal?.amount || 0) + tip.amount,
- });
- }
-
- return Array.from(supporterTotals.values()).sort((a, b) => b.amount - a.amount);
-}
-
-export const SupportersSection: FC = ({ tips = [], documentId, onTip }) => {
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [showAllSupporters, setShowAllSupporters] = useState(false);
- const { showUSD } = useCurrencyPreference();
-
- const consolidatedTips = consolidateTipsByUser(tips);
- const hasSupporters = consolidatedTips.length > 0;
- const displayLimit = 5; // Show only top 5 supporters in the sidebar
- const displayedSupporters = showAllSupporters
- ? consolidatedTips
- : consolidatedTips.slice(0, displayLimit);
- const hasMoreSupporters = consolidatedTips.length > displayLimit;
-
- const handleTipSuccess = (amount: number) => {
- // Close the modal and potentially refresh data
- setIsModalOpen(false);
- };
-
- const handleTipClick = () => {
- if (onTip) {
- onTip();
- } else {
- // Open the tip modal
- setIsModalOpen(true);
- }
- };
-
- return (
-
-
-
- {hasSupporters ? (
- <>
-
- {displayedSupporters.map((supporter) => (
-
-
-
-
- {supporter.user.fullName}
-
-
-
- +
-
-
-
- ))}
-
-
- {hasMoreSupporters && (
- setShowAllSupporters(!showAllSupporters)}
- className="text-sm text-blue-600 hover:text-blue-800 font-medium mt-3 w-full text-center"
- >
- {showAllSupporters ? 'Show less' : `View all supporters (${consolidatedTips.length})`}
-
- )}
-
-
-
-
- Add your support
-
-
- >
- ) : (
-
-
Support the authors with ResearchCoin
-
-
- Tip RSC
-
-
- )}
-
- {/* Tip Modal */}
- setIsModalOpen(false)}
- contentId={documentId}
- feedContentType="PAPER"
- onTipSuccess={handleTipSuccess}
- />
-
- );
-};
diff --git a/components/work/components/UpdatesSection.tsx b/components/work/components/UpdatesSection.tsx
deleted file mode 100644
index 0bbd365f6..000000000
--- a/components/work/components/UpdatesSection.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-'use client';
-
-import { Bell } from 'lucide-react';
-import { ProgressUpdates } from '@/components/ui/ProgressUpdates';
-import { calculateUpdateRate } from '@/components/Fund/lib/FundUtils';
-import { UpdateRateBadge } from '@/components/ui/badges/UpdateRateBadge';
-
-interface Update {
- id: number;
- createdDate: string;
- content?: any;
-}
-
-interface UpdatesSectionProps {
- updates: Update[];
- startDate?: string; // Project start date
- className?: string;
-}
-
-export const UpdatesSection = ({ updates = [], startDate, className }: UpdatesSectionProps) => {
- if (updates.length === 0 && !startDate) {
- return null;
- }
-
- const updateRate = calculateUpdateRate(updates);
-
- return (
-
-
-
-
-
Author Updates
-
-
-
-
-
-
-
-
- );
-};
diff --git a/constants/tokens.ts b/constants/tokens.ts
index 3d009b382..d3b2bef5f 100644
--- a/constants/tokens.ts
+++ b/constants/tokens.ts
@@ -52,21 +52,6 @@ export const RSC: Token = {
chainId: BASE_CHAIN_ID,
};
-/**
- * Get RSC token configuration for a specific network
- */
-export function getRSCForNetwork(network: NetworkType): Token {
- const config = NETWORK_CONFIG[network];
- return {
- name: 'ResearchCoin',
- symbol: 'RSC',
- decimals: 18,
- address: config.rscAddress as `0x${string}`,
- image: 'RSC.webp',
- chainId: config.chainId,
- };
-}
-
export const ETH: Token = {
name: 'ETH',
address: '',
@@ -86,16 +71,3 @@ export const USDC: Token = {
image: 'USDC.webp',
chainId: BASE_CHAIN_ID,
};
-
-export const TRANSFER_ABI = [
- {
- inputs: [
- { internalType: 'address', name: '_to', type: 'address' },
- { internalType: 'uint256', name: '_value', type: 'uint256' },
- ],
- name: 'transfer',
- outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
- stateMutability: 'nonpayable',
- type: 'function',
- },
-];
diff --git a/hooks/useFundingOverview.ts b/hooks/useFundingOverview.ts
deleted file mode 100644
index dc309f726..000000000
--- a/hooks/useFundingOverview.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-import { FunderService } from '@/services/funder.service';
-import { FunderOverview } from '@/types/funder';
-import { useUser } from '@/contexts/UserContext';
-
-interface UseFundingOverviewReturn {
- overview: FunderOverview | null;
- isLoading: boolean;
-}
-
-/**
- * Fetches the current user's funding overview (amount given, credits, supported
- * researchers, etc.). Resolves to `null` for signed-out users. Shared by the
- * funding snapshot surfaces (Fund, Earn, and ResearchCoin pages).
- */
-export function useFundingOverview(): UseFundingOverviewReturn {
- const { user, isLoading: isLoadingUser } = useUser();
- const [overview, setOverview] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
-
- useEffect(() => {
- if (isLoadingUser) return;
-
- if (!user) {
- setOverview(null);
- setIsLoading(false);
- return;
- }
-
- let cancelled = false;
- setIsLoading(true);
- FunderService.getFundingOverview(user.id)
- .then((data) => {
- if (!cancelled) setOverview(data);
- })
- .catch(() => {
- if (!cancelled) setOverview(null);
- })
- .finally(() => {
- if (!cancelled) setIsLoading(false);
- });
-
- return () => {
- cancelled = true;
- };
- }, [user, isLoadingUser]);
-
- return { overview, isLoading };
-}
diff --git a/hooks/useFundraise.ts b/hooks/useFundraise.ts
index d6170112f..15c5e31da 100644
--- a/hooks/useFundraise.ts
+++ b/hooks/useFundraise.ts
@@ -12,9 +12,6 @@ interface UseFundraiseState {
error: string | null;
}
-type CreateContributionFn = (id: ID, payload: any) => Promise;
-type UseCreateContributionReturn = [UseFundraiseState, CreateContributionFn];
-
type CloseFundraiseFn = (id: ID) => Promise;
type UseCloseFundraiseReturn = [UseFundraiseState, CloseFundraiseFn];
@@ -24,31 +21,6 @@ type UseCompleteFundraiseReturn = [UseFundraiseState, CompleteFundraiseFn];
type ReopenFundraiseFn = (id: ID, durationDays: number) => Promise;
type UseReopenFundraiseReturn = [UseFundraiseState, ReopenFundraiseFn];
-export const useCreateContribution = (): UseCreateContributionReturn => {
- const [data, setData] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const createContribution = async (id: ID, payload: any) => {
- setIsLoading(true);
- setError(null);
-
- try {
- const response = await FundraiseService.createContribution(id, payload);
- setData(response);
- } catch (err) {
- const { data = {} } = err instanceof ApiError ? JSON.parse(err.message) : {};
- const errorMsg = data?.message || 'Failed to create contribution';
- setError(errorMsg);
- throw err;
- } finally {
- setIsLoading(false);
- }
- };
-
- return [{ data, isLoading, error }, createContribution];
-};
-
export const useCloseFundraise = (): UseCloseFundraiseReturn => {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
diff --git a/hooks/useIsSafari.ts b/hooks/useIsSafari.ts
deleted file mode 100644
index 68c15b516..000000000
--- a/hooks/useIsSafari.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-'use client';
-
-import { useState, useEffect } from 'react';
-
-/**
- * Hook to detect if the browser is Safari (supports Apple Pay).
- * Works for both mobile and desktop Safari.
- *
- * @returns true if the browser is Safari, false otherwise
- */
-export function useIsSafari(): boolean {
- const [isSafari, setIsSafari] = useState(false);
-
- useEffect(() => {
- const ua = navigator.userAgent;
- // Safari but not Chrome (Chrome also includes Safari in UA)
- const isSafariBrowser = /Safari/.test(ua) && !/Chrome/.test(ua) && !/CriOS/.test(ua);
- setIsSafari(isSafariBrowser);
- }, []);
-
- return isSafari;
-}
diff --git a/hooks/useWalletRSCBalance.ts b/hooks/useWalletRSCBalance.ts
deleted file mode 100644
index eb34e5e0d..000000000
--- a/hooks/useWalletRSCBalance.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-'use client';
-
-import { useAccount, useBalance } from 'wagmi';
-import { useMemo } from 'react';
-import { getRSCForNetwork, NetworkType } from '@/constants/tokens';
-
-interface UseWalletRSCBalanceReturn {
- balance: number;
- balanceData: ReturnType['data'];
- isLoading: boolean;
- error: Error | null;
-}
-
-interface UseWalletRSCBalanceOptions {
- network?: NetworkType;
-}
-
-/**
- * Hook to check the connected wallet's RSC balance
- * @param options - Options including network selection
- * @returns An object containing the wallet's RSC balance and related states
- */
-export function useWalletRSCBalance(
- options: UseWalletRSCBalanceOptions = {}
-): UseWalletRSCBalanceReturn {
- const { network = 'BASE' } = options;
- const { address } = useAccount();
-
- const rscToken = useMemo(() => getRSCForNetwork(network), [network]);
-
- const {
- data: balanceData,
- isLoading,
- error,
- } = useBalance({
- address,
- token: rscToken.address?.startsWith('0x') ? (rscToken.address as `0x${string}`) : undefined,
- chainId: rscToken.chainId,
- });
-
- const balance = useMemo(
- () => (balanceData ? parseFloat(balanceData.formatted) : 0),
- [balanceData]
- );
-
- return {
- balance,
- balanceData,
- isLoading,
- error: error || null,
- };
-}
diff --git a/services/fundraise.service.ts b/services/fundraise.service.ts
index 0579cc256..139dd5eff 100644
--- a/services/fundraise.service.ts
+++ b/services/fundraise.service.ts
@@ -76,21 +76,6 @@ export class FundraiseService {
return transformFundraise(response);
}
- /**
- * Alias for contributeToFundraise to maintain backwards compatibility with existing hooks
- * @param id The ID of the fundraise to contribute to
- * @param payload The payload containing the amount and optional currency
- * @returns The updated fundraise
- */
- static async createContribution(id: ID, payload: any): Promise {
- return this.contributeToFundraise(
- id,
- payload.amount,
- payload.currency || 'rsc',
- payload.useCredits ?? false
- );
- }
-
/**
* Create a contribution to a fundraise via Endaoment (DAF).
* @param fundraiseId The ID of the fundraise to contribute to
diff --git a/store/grantStore.ts b/store/grantStore.ts
deleted file mode 100644
index 2dc434f24..000000000
--- a/store/grantStore.ts
+++ /dev/null
@@ -1,397 +0,0 @@
-import { Work } from '@/types/work';
-import { WorkMetadata } from '@/services/metadata.service';
-
-export interface GrantWithMetadata {
- work: Work;
- metadata: WorkMetadata;
-}
-
-// Define the applicant type for grants
-export interface GrantApplicant {
- id: number;
- fullName: string;
- profileImage: string;
- profileUrl: string;
- appliedDate: string;
- user?: {
- isVerified?: boolean;
- };
-}
-
-const baseDate = new Date();
-const futureDate = new Date(baseDate.setMonth(baseDate.getMonth() + 2));
-
-const mockGrant1: GrantWithMetadata = {
- work: {
- id: 999,
- type: 'funding_request',
- contentType: 'funding_request',
- title: 'Demo Quantum Computing Research Grant',
- slug: 'demo-quantum-computing-grant',
- shortDescription:
- 'Seeking researchers to develop scalable quantum error-correction protocols and demonstrate entanglement distribution across fiber networks.',
- createdDate: new Date().toISOString(),
- authors: [
- {
- authorProfile: {
- id: 1,
- fullName: 'Ada Lovelace',
- firstName: 'Ada',
- lastName: 'Lovelace',
- profileImage: '',
- headline: 'Computer Scientist',
- profileUrl: '#',
- isClaimed: false,
- user: {
- isVerified: true,
- },
- },
- isCorresponding: true,
- position: 'first',
- },
- {
- authorProfile: {
- id: 2,
- fullName: 'Helen Quinn',
- firstName: 'Helen',
- lastName: 'Quinn',
- profileImage: '',
- headline: 'Particle Physicist',
- profileUrl: '#',
- isClaimed: false,
- user: {
- isVerified: true,
- },
- },
- isCorresponding: true,
- position: 'first',
- },
- ],
- abstract: '',
- objectives: [
- 'Develop scalable quantum error-correction protocols that reduce qubit overhead by at least 50%.',
- 'Demonstrate entanglement distribution across a 100-km fiber network using quantum repeaters.',
- 'Open-source a simulator and benchmarking suite for hybrid quantum-classical algorithms.',
- ],
- eligibilityRequirements: [
- 'Quantum Computing Track Record: Published breakthroughs or prototypes in quantum hardware, algorithms, or networking within the last five years.',
- 'Interdisciplinary Collaboration: Active partnerships with at least one academic lab and one industry group engaged in quantum technologies.',
- 'Open Science Commitment: Willingness to publish intermediate results and datasets under permissive licenses.',
- ],
- opensDate: new Date().toISOString(),
- topics: [
- {
- id: 1,
- name: 'Quantum Computing',
- slug: 'quantum-computing',
- },
- ],
- formats: [],
- figures: [],
- metrics: {
- votes: 256,
- comments: 12,
- saves: 8,
- },
- note: {
- id: 1,
- access: 'PRIVATE',
- organization: {
- id: 1,
- coverImage: null,
- name: 'Quantum Research Foundation',
- slug: 'quantum-research-foundation',
- },
- createdDate: new Date().toISOString(),
- updatedDate: new Date().toISOString(),
- title: 'Grant Expectations',
- isRemoved: false,
- post: null,
- plainText:
- 'Recipients are expected to publish monthly progress reports and share data openly with the community.',
- versionId: 1,
- versionDate: new Date().toISOString(),
- contentJson: '{}',
- content: '',
- },
- // Add applicants to the work object
- applicants: [
- {
- id: 101,
- fullName: 'Dr. Sarah Chen',
- profileImage: '/images/avatars/sarah-chen.jpg',
- profileUrl: '/profile/sarah-chen',
- appliedDate: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
- user: { isVerified: true },
- },
- {
- id: 102,
- fullName: 'Prof. Marcus Williams',
- profileImage: '/images/avatars/marcus-williams.jpg',
- profileUrl: '/profile/marcus-williams',
- appliedDate: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
- user: { isVerified: true },
- },
- {
- id: 103,
- fullName: 'Dr. Elena Rodriguez',
- profileImage: '/images/avatars/elena-rodriguez.jpg',
- profileUrl: '/profile/elena-rodriguez',
- appliedDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
- user: { isVerified: false },
- },
- {
- id: 104,
- fullName: 'Dr. James Park',
- profileImage: '/images/avatars/james-park.jpg',
- profileUrl: '/profile/james-park',
- appliedDate: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
- user: { isVerified: true },
- },
- {
- id: 105,
- fullName: 'Dr. Priya Sharma',
- profileImage: '/images/avatars/priya-sharma.jpg',
- profileUrl: '/profile/priya-sharma',
- appliedDate: new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString(),
- user: { isVerified: true },
- },
- ] as GrantApplicant[],
- } as unknown as Work,
- metadata: {
- id: 999,
- score: 256,
- topics: [
- {
- id: 1,
- name: 'Quantum Computing',
- slug: 'quantum-computing',
- },
- ],
- metrics: {
- votes: 256,
- comments: 12,
- saves: 8,
- reviewScore: 0,
- },
- fundraising: {
- id: 1,
- status: 'OPEN',
- goalCurrency: 'USD',
- goalAmount: {
- usd: 250000,
- rsc: 0,
- },
- amountRaised: {
- usd: 0,
- rsc: 18500,
- },
- startDate: new Date().toISOString(),
- endDate: futureDate.toISOString(),
- contributors: {
- numContributors: 42,
- topContributors: [],
- },
- createdDate: new Date().toISOString(),
- updatedDate: new Date().toISOString(),
- },
- bounties: [],
- activeBounties: 0,
- closedBounties: 0,
- },
-};
-
-const mockGrant2: GrantWithMetadata = {
- work: {
- id: 1000,
- type: 'funding_request',
- contentType: 'funding_request',
- title: 'Climate Change AI Research Initiative',
- slug: 'climate-change-ai-research',
- shortDescription:
- 'Building machine learning models to predict extreme weather events and create early warning systems for vulnerable coastal communities.',
- createdDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days ago
- authors: [
- {
- authorProfile: {
- id: 3,
- fullName: 'Katherine Johnson',
- firstName: 'Katherine',
- lastName: 'Johnson',
- profileImage: '',
- headline: 'Applied Mathematician',
- profileUrl: '#',
- isClaimed: false,
- user: {
- isVerified: true,
- },
- },
- isCorresponding: true,
- position: 'first',
- },
- {
- authorProfile: {
- id: 4,
- fullName: 'Yann LeCun',
- firstName: 'Yann',
- lastName: 'LeCun',
- profileImage: '',
- headline: 'AI Researcher',
- profileUrl: '#',
- isClaimed: false,
- user: {
- isVerified: true,
- },
- },
- isCorresponding: false,
- position: 'last',
- },
- ],
- abstract: '',
- objectives: [
- 'Develop machine learning models to predict extreme weather events with 90% accuracy.',
- 'Create an open dataset of climate variables from satellite imagery spanning 50 years.',
- 'Design automated early warning systems for coastal communities facing sea-level rise.',
- 'Establish partnerships with 10+ meteorological agencies for real-time data sharing.',
- ],
- eligibilityRequirements: [
- 'AI/ML Expertise: Demonstrated experience in deep learning, computer vision, or time-series analysis with climate data.',
- 'Climate Science Background: Collaboration with atmospheric scientists, oceanographers, or environmental researchers.',
- 'Data Infrastructure: Access to high-performance computing resources and experience with large-scale data processing.',
- 'Community Impact: Commitment to deploying solutions in vulnerable communities and measuring real-world outcomes.',
- ],
- opensDate: new Date().toISOString(),
- topics: [
- {
- id: 2,
- name: 'Climate Science',
- slug: 'climate-science',
- },
- {
- id: 3,
- name: 'Artificial Intelligence',
- slug: 'artificial-intelligence',
- },
- ],
- formats: [],
- figures: [],
- metrics: {
- votes: 189,
- comments: 8,
- saves: 15,
- },
- note: {
- id: 2,
- access: 'PRIVATE',
- organization: {
- id: 2,
- coverImage: null,
- name: 'Global Climate Foundation',
- slug: 'global-climate-foundation',
- },
- createdDate: new Date().toISOString(),
- updatedDate: new Date().toISOString(),
- title: 'Grant Expectations',
- isRemoved: false,
- post: null,
- plainText:
- 'Funded projects must prioritize open science practices and demonstrate measurable environmental impact within 18 months.',
- versionId: 1,
- versionDate: new Date().toISOString(),
- contentJson: '{}',
- content: '',
- },
- // Add applicants to the work object
- applicants: [
- {
- id: 201,
- fullName: 'Dr. Alex Thompson',
- profileImage: '/images/avatars/alex-thompson.jpg',
- profileUrl: '/profile/alex-thompson',
- appliedDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
- user: { isVerified: true },
- },
- {
- id: 202,
- fullName: 'Prof. Lisa Zhang',
- profileImage: '/images/avatars/lisa-zhang.jpg',
- profileUrl: '/profile/lisa-zhang',
- appliedDate: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
- user: { isVerified: true },
- },
- {
- id: 203,
- fullName: 'Dr. Mohammed Al-Rashid',
- profileImage: '/images/avatars/mohammed-al-rashid.jpg',
- profileUrl: '/profile/mohammed-al-rashid',
- appliedDate: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
- user: { isVerified: false },
- },
- ] as GrantApplicant[],
- } as unknown as Work,
- metadata: {
- id: 1000,
- score: 189,
- topics: [
- {
- id: 2,
- name: 'Climate Science',
- slug: 'climate-science',
- },
- {
- id: 3,
- name: 'Artificial Intelligence',
- slug: 'artificial-intelligence',
- },
- ],
- metrics: {
- votes: 189,
- comments: 8,
- saves: 15,
- reviewScore: 0,
- },
- fundraising: {
- id: 2,
- status: 'OPEN',
- goalCurrency: 'USD',
- goalAmount: {
- usd: 500000,
- rsc: 0,
- },
- amountRaised: {
- usd: 0,
- rsc: 12300,
- },
- startDate: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(), // 3 days ago
- endDate: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000).toISOString(), // 45 days from now
- contributors: {
- numContributors: 28,
- topContributors: [],
- },
- createdDate: new Date().toISOString(),
- updatedDate: new Date().toISOString(),
- },
- bounties: [],
- activeBounties: 0,
- closedBounties: 0,
- },
-};
-
-// Store with all mock grants
-export const mockGrants: GrantWithMetadata[] = [mockGrant1, mockGrant2];
-
-// Helper functions
-export const getGrantById = (id: number): GrantWithMetadata | undefined => {
- return mockGrants.find((grant) => grant.work.id === id);
-};
-
-export const getOpenGrants = (): GrantWithMetadata[] => {
- return mockGrants.filter((grant) => grant.metadata.fundraising?.status === 'OPEN');
-};
-
-export const getClosedGrants = (): GrantWithMetadata[] => {
- return mockGrants.filter((grant) => grant.metadata.fundraising?.status === 'CLOSED');
-};
-
-// Default export for backwards compatibility
-export const defaultMockGrant = mockGrant1;
diff --git a/types/endaoment.ts b/types/endaoment.ts
deleted file mode 100644
index 3f2ba4e5c..000000000
--- a/types/endaoment.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Endaoment DAF (Donor Advised Fund) account types
- */
-
-export interface EndaomentDAFAccount {
- id: string;
- name: string;
- /** Balance in USD */
- balanceUsd: number;
-}
-
-export interface EndaomentUser {
- id: string;
- email: string;
- accounts: EndaomentDAFAccount[];
-}
diff --git a/types/grant.ts b/types/grant.ts
index 63c2592d8..6bc390833 100644
--- a/types/grant.ts
+++ b/types/grant.ts
@@ -12,29 +12,6 @@ export const GRANT_IMAGE_FALLBACK_GRADIENT =
'radial-gradient(ellipse at 25% 35%, rgba(251,146,60,0.55) 0%, transparent 50%), ' +
'linear-gradient(135deg, #1a1a2e 0%, #0f3460 100%)';
-export const GRANT_STATUS_CONFIG: Record<
- GrantStatus,
- { dotClass: string; badgeClass: string; label: string }
-> = {
- OPEN: {
- dotClass: 'bg-emerald-500',
- badgeClass: 'text-primary-700 bg-primary-100',
- label: 'Accepting Applications',
- },
- CLOSED: { dotClass: 'bg-gray-400', badgeClass: 'text-gray-500 bg-gray-200', label: 'Closed' },
- PENDING: {
- dotClass: 'bg-yellow-500',
- badgeClass: 'text-yellow-700 bg-yellow-100',
- label: 'Pending Review',
- },
- DECLINED: { dotClass: 'bg-red-500', badgeClass: 'text-red-700 bg-red-100', label: 'Declined' },
- COMPLETED: {
- dotClass: 'bg-blue-500',
- badgeClass: 'text-blue-700 bg-blue-100',
- label: 'Completed',
- },
-};
-
export interface GrantAmount {
usd: number;
rsc: number;