Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/notebook/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { LeftSidebar as MainLeftSidebar } from '../layouts/LeftSidebar';
import { NotebookProvider } from '@/contexts/NotebookContext';
import { useScreenSize } from '@/hooks/useScreenSize';
import { NoteEditorLayout } from '@/components/Notebook/NoteEditorLayout';
import { clearPendingGrant } from '@/components/Editor/lib/utils/publishingFormStorage';

function NotebookLayoutContent({ children }: { children: ReactNode }) {
const { isLeftSidebarOpen, closeLeftSidebar, openLeftSidebar, closeBothSidebars } = useSidebar();
Expand All @@ -37,6 +38,10 @@ function NotebookLayoutContent({ children }: { children: ReactNode }) {
}
}, [lgAndUp, openLeftSidebar, closeBothSidebars]);

useEffect(() => {
return () => clearPendingGrant();
}, []);

if (isDesktop === null) {
return null;
}
Expand Down
39 changes: 39 additions & 0 deletions components/Editor/lib/utils/publishingFormStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,42 @@ export const clearPublishingFormStorage = (noteId: string) => {
console.error('Error clearing publishing form from localStorage:', error);
}
};

const PENDING_GRANT_KEY = 'pendingGrant';

export interface SelectedGrantData {
id: string;
shortTitle: string;
imageUrl: string;
fundingAmount: number;
organization: string;
}

export const setPendingGrant = (grant: SelectedGrantData) => {
if (globalThis.window === undefined) return;
try {
sessionStorage.setItem(PENDING_GRANT_KEY, JSON.stringify(grant));
} catch (error) {
console.error('Error saving pending grant:', error);
}
};

export const getPendingGrant = (): SelectedGrantData | null => {
if (globalThis.window === undefined) return null;
try {
const raw = sessionStorage.getItem(PENDING_GRANT_KEY);
return raw ? JSON.parse(raw) : null;
} catch (error) {
console.error('Error reading pending grant:', error);
return null;
}
};

export const clearPendingGrant = () => {
if (globalThis.window === undefined) return;
try {
sessionStorage.removeItem(PENDING_GRANT_KEY);
} catch (error) {
console.error('Error clearing pending grant:', error);
}
};
3 changes: 3 additions & 0 deletions components/Funding/GrantInfoBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@ export const GrantInfoBanner = ({
grantId={grantId}
grantTitle={work?.title}
grantAmountUsd={amountUsd}
grantShortTitle={work?.note?.post?.grant?.shortTitle}
grantImageUrl={work?.image}
grantOrganization={organization}
/>
)}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useFormContext } from 'react-hook-form';
import { Upload, Image as ImageIcon, Gift } from 'lucide-react';
import { Upload, Image as ImageIcon, Gift, X, Plus } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/form/Input';
import { Switch } from '@/components/ui/Switch';
Expand All @@ -11,13 +11,89 @@ import { FundraiseSection } from '@/components/work/components/FundraiseSection'
import { NonprofitSearchSection } from '@/components/Nonprofit';
import { useNonprofitByFundraiseId } from '@/hooks/useNonprofitByFundraiseId';
import { useNonprofitSearch } from '@/hooks/useNonprofitSearch';
import { SelectFundingOpportunityModal } from '@/components/modals/SelectFundingOpportunityModal';
import { SelectedGrantData } from '@/components/Editor/lib/utils/publishingFormStorage';
import { formatCompactAmount } from '@/utils/currency';
import { GRANT_IMAGE_FALLBACK_GRADIENT } from '@/types/grant';

interface FundingSectionProps {
note: Note;
}

const FEATURE_FLAG_NFT_REWARDS = false;

function FundingOpportunitySection() {
const { watch, setValue } = useFormContext();
const selectedGrant: SelectedGrantData | null = watch('selectedGrant');
const workId = watch('workId');
const [isModalOpen, setIsModalOpen] = useState(false);

if (workId) return null;

return (
<>
<div>
<h3 className="text-[15px] font-semibold tracking-tight text-gray-900 mb-2">
Funding Opportunity <span className="font-normal text-gray-500 text-xs">(Optional)</span>
</h3>
{selectedGrant ? (
<div className="flex gap-3 p-3 rounded-xl border border-gray-200 bg-gray-50 relative">
<div className="w-12 h-12 rounded-lg overflow-hidden bg-gray-900 flex-shrink-0 relative">
{selectedGrant.imageUrl ? (
<Image
src={selectedGrant.imageUrl}
alt={selectedGrant.shortTitle}
fill
className="object-cover"
sizes="48px"
/>
) : (
<div
className="absolute inset-0"
style={{ background: GRANT_IMAGE_FALLBACK_GRADIENT }}
/>
)}
</div>
<div className="flex-1 min-w-0">
<div className="text-[10px] font-semibold uppercase tracking-wider text-gray-400">
{selectedGrant.organization || 'ResearchHub Grant'}
</div>
<div className="text-sm font-semibold text-gray-900 truncate">
{selectedGrant.shortTitle}
</div>
<div className="text-xs font-medium text-emerald-600">
{formatCompactAmount(selectedGrant.fundingAmount)} Funding
</div>
</div>
<button
type="button"
onClick={() => setValue('selectedGrant', null)}
className="absolute top-2 right-2 p-1 rounded-full hover:bg-gray-200 transition-colors text-gray-400 hover:text-gray-600"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
) : (
<button
type="button"
onClick={() => setIsModalOpen(true)}
className="w-full flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg border border-dashed border-gray-300 text-xs text-gray-500 hover:border-blue-300 hover:text-blue-600 hover:bg-blue-50/50 transition-colors"
>
<Plus className="w-3.5 h-3.5" />
Select Funding Opportunity
</button>
)}
</div>

<SelectFundingOpportunityModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSelect={(grant) => setValue('selectedGrant', grant)}
/>
</>
);
}

export function FundingSection({ note }: Readonly<FundingSectionProps>) {
const {
register,
Expand Down Expand Up @@ -103,6 +179,8 @@ export function FundingSection({ note }: Readonly<FundingSectionProps>) {

return (
<div className="py-3 px-6 space-y-6">
<FundingOpportunitySection />

{fundraise ? (
<>
<FundraiseSection fundraise={fundraise} />
Expand Down
14 changes: 14 additions & 0 deletions components/Notebook/PublishingForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { toast } from 'react-hot-toast';
import {
loadPublishingFormFromStorage,
savePublishingFormToStorage,
getPendingGrant,
clearPendingGrant,
} from '@/components/Editor/lib/utils/publishingFormStorage';
import { PublishingFormSkeleton } from '@/components/skeletons/PublishingFormSkeleton';
import { Loader2 } from 'lucide-react';
Expand Down Expand Up @@ -98,6 +100,7 @@ const FORM_DEFAULTS = {
budget: '',
coverImage: null,
selectedNonprofit: null,
selectedGrant: null,
departmentLabName: '',
shortDescription: '',
organization: '',
Expand Down Expand Up @@ -278,6 +281,13 @@ export function PublishingForm({

applyGrantDefaults(methods.getValues, methods.setValue);
autoAddCurrentUser(methods.getValues, methods.setValue, currentUser);

const pending = getPendingGrant();
if (pending) {
methods.setValue('selectedGrant', pending);
clearPendingGrant();
}

savePublishingFormToStorage(
note.id.toString(),
methods.getValues() as Partial<PublishingFormData>
Expand Down Expand Up @@ -426,6 +436,9 @@ export function PublishingForm({
budgetValue = formData.budget || '0';
}

const isNewProposal = formData.articleType === 'preregistration' && !formData.workId;
const grantId = isNewProposal ? (formData.selectedGrant?.id ?? null) : null;

const response = await upsertPost(
{
budget: budgetValue,
Expand Down Expand Up @@ -454,6 +467,7 @@ export function PublishingForm({
formData.articleType === 'grant'
? new Date('2029-12-31')
: formData.applicationDeadline,
grantId,
},
formData.workId
);
Expand Down
1 change: 1 addition & 0 deletions components/Notebook/PublishingForm/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const publishingFormSchema = z
.optional(),
isJournalEnabled: z.boolean().optional(),
selectedNonprofit: z.any().nullable().optional(),
selectedGrant: z.any().nullable().optional(),
departmentLabName: z.string().optional(),
shortDescription: z.string().optional(),
organization: z.string().optional(),
Expand Down
20 changes: 14 additions & 6 deletions components/modals/ApplyToGrantModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/Badge';
import { Tooltip } from '@/components/ui/Tooltip';
import { PostService, ProposalForModal } from '@/services/post.service';
import { GrantService } from '@/services/grant.service';
import { setPendingGrant } from '@/components/Editor/lib/utils/publishingFormStorage';
import { useUser } from '@/contexts/UserContext';
import { useRouter } from 'next/navigation';
import { toast } from 'react-hot-toast';
Expand All @@ -28,19 +29,16 @@ const ProposalSkeleton = () => (
</div>
);

function formatCompactAmount(usd: number): string {
if (usd >= 1_000_000) return `$${Math.round(usd / 1_000_000)}M`;
if (usd >= 1_000) return `$${Math.round(usd / 1_000)}K`;
return `$${Math.round(usd).toLocaleString()}`;
}

interface ApplyToGrantModalProps {
isOpen: boolean;
onClose: () => void;
onUseSelected: (proposal: ProposalForModal) => void;
grantId: string;
grantTitle?: string;
grantAmountUsd?: number;
grantShortTitle?: string;
grantImageUrl?: string;
grantOrganization?: string;
}

export const ApplyToGrantModal: React.FC<ApplyToGrantModalProps> = ({
Expand All @@ -50,6 +48,9 @@ export const ApplyToGrantModal: React.FC<ApplyToGrantModalProps> = ({
grantId,
grantTitle,
grantAmountUsd,
grantShortTitle,
grantImageUrl,
grantOrganization,
}) => {
const [proposals, setProposals] = useState<ProposalForModal[]>([]);
const [selectedProposalId, setSelectedProposalId] = useState<string | null>(null);
Expand All @@ -72,6 +73,13 @@ export const ApplyToGrantModal: React.FC<ApplyToGrantModalProps> = ({
};

const handleDraftNew = () => {
setPendingGrant({
id: grantId,
shortTitle: grantShortTitle || grantTitle || '',
imageUrl: grantImageUrl || '',
fundingAmount: grantAmountUsd || 0,
organization: grantOrganization || '',
});
onClose();
router.push('/notebook?newFunding=true');
};
Expand Down
Loading