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 ( - -
-
{icon}
-
{title}
-
-
- ); -} - -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 ? ( - - ) : onDetailsClick ? ( - - ) : null} -
-
- - {/* Progress bar */} - -
- ) : ( -
-
-
-
- - raised of - - goal -
-
{getStatusDisplay()}
-
- -
- -
- {isActive ? ( - - ) : onDetailsClick ? ( - - ) : 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 */} -
- - {isActive && ( - - )} -
-
-
- ); -}; 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 */} -
- - - {voteCount} - - -
- - {/* Save to list */} - {work.unifiedDocumentId && ( - - )} - - {/* 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

-
- ) : ( - -
-

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 */} -
- - {isActive && ( - - )} -
-
-
- ); -}; 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 */} -
-
-
- -
-

Top Funders

-
-
-
- View All - -
-
- -
- - {/* 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 */} -
-
-
- -
-

Top Funders

-
-
- - View All - - -
- {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 = ( - - ); - - return ( -
-
- - {organizations.map((org) => { - const isSelected = org.slug === selectedOrg?.slug; - return ( - handleOrgSelect(org)} - className={isSelected ? 'bg-gray-50 cursor-default' : ''} - > -
- - - {org.name} - - {isSelected && } -
-
- ); - })} -
-
- -
- -
- - {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 ( -
-
- - {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