diff --git a/app/author/[id]/components/Moderation.tsx b/app/author/[id]/components/Moderation.tsx index 787255a6d..828a2c033 100644 --- a/app/author/[id]/components/Moderation.tsx +++ b/app/author/[id]/components/Moderation.tsx @@ -14,7 +14,7 @@ export function ModerationSkeleton() { return (
-
+
{Array.from({ length: 5 }).map((_, i) => (
@@ -232,7 +232,7 @@ export default function Moderation({ userId, authorId, refetchAuthorInfo }: Mode
-
+
Email: {userDetails.email || 'N/A'} diff --git a/app/author/[id]/page.tsx b/app/author/[id]/page.tsx index 5b29b5957..95c7aef67 100644 --- a/app/author/[id]/page.tsx +++ b/app/author/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import { use, useTransition } from 'react'; +import { use, useTransition, useState } from 'react'; import { useAuthorAchievements, useAuthorInfo, useAuthorSummaryStats } from '@/hooks/useAuthor'; import { useUser } from '@/contexts/UserContext'; import { Card } from '@/components/ui/Card'; @@ -18,6 +18,10 @@ import AuthorProfile from './components/AuthorProfile'; import { useAuthorPublications } from '@/hooks/usePublications'; import { transformPublicationToFeedEntry } from '@/types/publication'; import PinnedFundraise from './components/PinnedFundraise'; +import { BookOpen } from 'lucide-react'; +import { Button } from '@/components/ui/Button'; +import { toast } from 'react-hot-toast'; +import { AddPublicationsModal } from '@/components/modals/AddPublicationsModal'; function toNumberOrNull(value: any): number | null { if (value === '' || value === null || value === undefined) return null; @@ -90,8 +94,18 @@ const TAB_TO_CONTRIBUTION_TYPE: Record = { bounties: 'BOUNTY', }; -function AuthorTabs({ authorId, userId }: { authorId: number; userId?: number }) { +function AuthorTabs({ + authorId, + isOwnProfile, + userId, +}: { + authorId: number; + isOwnProfile: boolean; + userId?: number; +}) { const [isPending, startTransition] = useTransition(); + const [isAddPublicationsModalOpen, setIsAddPublicationsModalOpen] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); const tabs = [ { id: 'contributions', label: 'Overview' }, { id: 'publications', label: 'Publications' }, @@ -126,10 +140,20 @@ function AuthorTabs({ authorId, userId }: { authorId: number; userId?: number }) hasMore: hasMorePublications, loadMore: loadMorePublications, isLoadingMore: isLoadingMorePublications, + refresh: refreshPublications, } = useAuthorPublications({ authorId, }); + const handleRefreshPublications = async () => { + setIsRefreshing(true); + try { + await refreshPublications(); + } finally { + setIsRefreshing(false); + } + }; + const handleTabChange = (tabId: string) => { startTransition(() => { const params = new URLSearchParams(searchParams); @@ -143,7 +167,6 @@ function AuthorTabs({ authorId, userId }: { authorId: number; userId?: number }) if (publicationsError) { return
Error: {publicationsError.message}
; } - // Filter out invalid publications const validPublications = publications.filter((publication) => { try { @@ -157,15 +180,28 @@ function AuthorTabs({ authorId, userId }: { authorId: number; userId?: number }) return (
+ {/* Add Publications Button - only show for own profile */} + {isOwnProfile && validPublications.length > 0 && ( +
+ +
+ )} + transformPublicationToFeedEntry(publication) ) } - isLoading={isPending || isPublicationsLoading} + isLoading={isPending || isPublicationsLoading || isRefreshing} hasMore={hasMorePublications} loadMore={loadMorePublications} showBountyFooter={false} @@ -173,7 +209,26 @@ function AuthorTabs({ authorId, userId }: { authorId: number; userId?: number }) isLoadingMore={isLoadingMorePublications} showBountySupportAndCTAButtons={false} showBountyDeadline={false} - noEntriesElement={} + noEntriesElement={ + isOwnProfile ? ( +
+ +

No publications yet

+

+ Add your published papers to showcase your research contributions. +

+ +
+ ) : ( + + ) + } maxLength={150} />
@@ -229,6 +284,16 @@ function AuthorTabs({ authorId, userId }: { authorId: number; userId?: number }) className="border-b" />
{renderTabContent()}
+ + {/* Add Publications Modal */} + setIsAddPublicationsModalOpen(false)} + onPublicationsAdded={() => { + toast.success('Publications added successfully!'); + handleRefreshPublications(); // Use the wrapper function + }} + />
); } @@ -239,6 +304,7 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st const authorId = toNumberOrNull(resolvedParams.id); const [{ author: user, isLoading, error }, refetchAuthorInfo] = useAuthorInfo(authorId); const { user: currentUser } = useUser(); + // Determine if current user is a hub editor const isHubEditor = !!currentUser?.authorProfile?.isHubEditor; const [{ achievements, isLoading: isAchievementsLoading, error: achievementsError }] = @@ -304,7 +370,11 @@ export default function AuthorProfilePage({ params }: { params: Promise<{ id: st )}
- + ); } diff --git a/components/modals/AddPublicationsModal.tsx b/components/modals/AddPublicationsModal.tsx new file mode 100644 index 000000000..9b82f20fe --- /dev/null +++ b/components/modals/AddPublicationsModal.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { BookOpen } from 'lucide-react'; +import { BaseModal } from '@/components/ui/BaseModal'; +import { AddPublicationsForm, STEP } from './Verification/AddPublicationsForm'; + +interface AddPublicationsModalProps { + isOpen: boolean; + onClose: () => void; + onPublicationsAdded?: () => void; +} + +export function AddPublicationsModal({ + isOpen, + onClose, + onPublicationsAdded, +}: AddPublicationsModalProps) { + const [currentStep, setCurrentStep] = useState('DOI'); + + useEffect(() => { + if (isOpen) { + setCurrentStep('DOI'); + } + }, [isOpen]); + + const handleStepChange = ({ step }: { step: STEP }) => { + console.log('handleStepChange', step); + setCurrentStep(step); + + if (step === 'FINISHED') { + onPublicationsAdded?.(); + onClose(); + } + }; + + const getModalTitle = () => { + switch (currentStep) { + case 'DOI': + return 'Add Publications to Your Profile'; + case 'NEEDS_AUTHOR_CONFIRMATION': + return 'Confirm Author Identity'; + case 'RESULTS': + return 'Select Publications'; + case 'LOADING': + return 'Adding Publications'; + case 'ERROR': + return 'Error'; + default: + return 'Add Publications'; + } + }; + + const getModalDescription = () => { + switch (currentStep) { + case 'DOI': + return "Enter a DOI for any paper you've published and we will fetch your other works."; + case 'NEEDS_AUTHOR_CONFIRMATION': + return 'We found multiple authors for this publication. Please select which one is you.'; + case 'RESULTS': + return 'Review and select the publications you want to add to your profile.'; + case 'LOADING': + return 'We are processing your publications. This may take a few minutes.'; + case 'ERROR': + return 'Something went wrong while adding your publications.'; + default: + return ''; + } + }; + + const headerAction = ( +
+ +
+ ); + + return ( + + {getModalDescription() && ( +

{getModalDescription()}

+ )} + + +
+ ); +} diff --git a/components/modals/Verification/AddPublicationsForm.tsx b/components/modals/Verification/AddPublicationsForm.tsx index 9a8068e6f..ec0891a76 100644 --- a/components/modals/Verification/AddPublicationsForm.tsx +++ b/components/modals/Verification/AddPublicationsForm.tsx @@ -85,7 +85,7 @@ export function AddPublicationsForm({ useEffect(() => { onStepChange?.({ step }); setError(null); - }, [step, onStepChange]); + }, [step]); // Handle errors from hooks useEffect(() => { diff --git a/components/ui/BaseModal.tsx b/components/ui/BaseModal.tsx index 86bb1b4ed..335ac5201 100644 --- a/components/ui/BaseModal.tsx +++ b/components/ui/BaseModal.tsx @@ -96,7 +96,7 @@ export const BaseModal: FC = ({ // No rounded corners on mobile, rounded on md+ 'md:!rounded-2xl', // Only apply max width on md and up - `md:${maxWidth}` + `md:!${maxWidth}` )} style={{ display: 'flex', diff --git a/services/websocket.service.ts b/services/websocket.service.ts index d0fafa622..259e1ca38 100644 --- a/services/websocket.service.ts +++ b/services/websocket.service.ts @@ -21,21 +21,6 @@ export interface WebSocketOptions { onError?: (error: Error) => void; } -const ALLOWED_ORIGINS = [ - 'localhost', - 'localhost:8000', - 'ws://localhost:8000', - 'ws://localhost:8000/', - 'backend.prod.researchhub.com', - 'wss://backend.prod.researchhub.com', - 'backend.staging.researchhub.com', - 'wss://backend.staging.researchhub.com', - 'v2.staging.researchhub.com', - 'wss://v2.staging.researchhub.com', - 'researchhub.com', - 'wss://researchhub.com', -]; - const CLOSE_CODES = { GOING_AWAY: 1001, POLICY_VIOLATION: 1008, diff --git a/tailwind.config.ts b/tailwind.config.ts index 5dc204c1b..55e916be4 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -7,7 +7,21 @@ export default { './components/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}', ], - safelist: ['md:max-w-md', 'md:max-w-lg'], + safelist: [ + 'md:!max-w-xs', + 'md:!max-w-md', + 'md:!max-w-lg', + 'md:!max-w-xl', + 'md:!max-w-2xl', + 'md:!max-w-3xl', + 'md:!max-w-4xl', + 'md:!max-w-5xl', + 'md:!max-w-6xl', + 'md:!max-w-7xl', + 'md:!max-w-full', + 'md:!max-w-screen', + 'md:!max-w-tablet', + ], darkMode: 'class', theme: { extend: {