Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fccfab2
Start screen: intent tabs on the composer, profile and RFP as chips
yattias Sep 22, 2026
e480a40
My Funding: totals in the rail, workspace composer under the tabs
yattias Sep 22, 2026
4fd1a3f
Enter the workspace from Publish and My Funding, not the nav
yattias Sep 22, 2026
c35755d
Workspace strip names what is open; funders get a concierge card
yattias Sep 22, 2026
0d69925
Draft rows carry their status; the journey timeline connects
yattias Sep 23, 2026
5946da1
My Funding lists RFPs and proposals as one kind of row
yattias Sep 23, 2026
b97acc2
My Funding rows: live work first, with its image and its money
yattias Sep 23, 2026
0bb84d8
Focus the composer on a new conversation; one form input per field
yattias Sep 23, 2026
110ab8b
RFP guidelines, workspace steps aside on publish, deletable drafts
yattias Sep 23, 2026
64b243e
Draft rows: delete lives behind a three-dots menu
yattias Sep 23, 2026
9bd7ff9
Start screen: words-only timeline, privacy line under the composer
yattias Sep 23, 2026
fd2c9bc
Start screen cards, "Let's get started", RFP hero tabs beside the pool
yattias Sep 23, 2026
957644f
RFP hero footnote breathes; pool contribution says what it does
yattias Sep 23, 2026
ded7aec
Pool modal: the note sits under the amount, with coins; no raised line
yattias Sep 23, 2026
53c639a
Revert the Publish menu and homepage tab icons to the originals
yattias Sep 23, 2026
3422208
Document pane: the drafting row says only "Drafting"
yattias Sep 23, 2026
5c96d8a
Workspace: the start screen is "New RFP" or "New proposal"
yattias Sep 23, 2026
7e59282
ESLint skips Playwright's output folders
yattias Oct 4, 2026
0e4b631
Activity wears a live dot: the RFP tab and the Recent Activity rail
yattias Oct 4, 2026
fd4fcf8
My Funding is one dashboard: Overview and Activity under a hero
yattias Oct 4, 2026
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
32 changes: 5 additions & 27 deletions app/layouts/MobileBottomNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
faBars,
} from '@fortawesome/pro-light-svg-icons';
import { faXTwitter, faDiscord, faGithub, faLinkedin } from '@fortawesome/free-brands-svg-icons';
import { BriefcaseBusiness, Sprout, Star } from 'lucide-react';
import { Sprout, Star } from 'lucide-react';
import { ChangelogLink } from '@/components/changelog/ChangelogLink';
import { FundingPowerBar } from '@/components/Funding/FundingPowerBar';
import { Icon } from '@/components/ui/icons';
Expand All @@ -24,10 +24,6 @@ import { useAuthenticatedAction } from '@/contexts/AuthModalContext';
import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext';
import { useScrollContainer } from '@/contexts/ScrollContainerContext';
import { isHomeTabPath } from '@/hooks/useFundTabs';
import { useUser } from '@/contexts/UserContext';
import { useOptionalAIMode } from '@/components/AIMode/AIModeContext';
import { AI_MODE_NAME } from '@/components/AIMode/copy';
import { isHubEditorOrModerator } from '@/utils/permissions';

interface NavItem {
label: string;
Expand All @@ -36,8 +32,6 @@ interface NavItem {
isMore?: boolean;
requiresAuth?: boolean;
isHome?: boolean;
/** Opens the workspace in place, on its new-conversation screen, instead of navigating. */
isAIMode?: boolean;
}

// Additional navigation items not in the bottom bar
Expand Down Expand Up @@ -76,17 +70,11 @@ export const MobileBottomNav: React.FC = () => {
const { executeAuthenticatedAction } = useAuthenticatedAction();
const { showUSD, toggleCurrency } = useCurrencyPreference();
const scrollContainerRef = useScrollContainer();
const { user } = useUser();
const aiMode = useOptionalAIMode();

// Moderators and hub editors, the only users the workspace admits, get it
// in the bar where Peer Review sits for everyone else.
const mainNavItems: NavItem[] = [
{ label: 'Home', href: '/', iconKey: 'home', isHome: true },
{ label: 'My Funding', href: '/my-funding', iconKey: 'fund', requiresAuth: true },
isHubEditorOrModerator(user)
? { label: AI_MODE_NAME, iconKey: 'workspace', isAIMode: true }
: { label: 'Peer Review', href: '/peer-review', iconKey: 'peer-review' },
{ label: 'Peer Review', href: '/peer-review', iconKey: 'peer-review' },
{ label: 'Wallet', href: '/researchcoin', iconKey: 'wallet' },
{ label: 'More', isMore: true, iconKey: 'more' },
];
Expand Down Expand Up @@ -117,10 +105,6 @@ export const MobileBottomNav: React.FC = () => {
setIsMoreOpen(true);
return;
}
if (item.isAIMode) {
aiMode?.selectChat(null);
return;
}

if (item.requiresAuth) {
executeAuthenticatedAction(() => router.push(item.href!));
Expand Down Expand Up @@ -151,10 +135,6 @@ export const MobileBottomNav: React.FC = () => {
color={iconColor}
/>
);
case 'workspace':
return (
<BriefcaseBusiness size={iconSize} color={iconColor} strokeWidth={isActive ? 2.25 : 2} />
);
case 'peer-review':
return (
<Star
Expand Down Expand Up @@ -248,11 +228,9 @@ export const MobileBottomNav: React.FC = () => {
{mainNavItems.map((item) => {
const isActive = item.isMore
? isMoreActive || isMoreOpen
: item.isAIMode
? Boolean(aiMode?.isOpen)
: item.href
? isPathActive(item.href, pathname, item.isHome)
: false;
: item.href
? isPathActive(item.href, pathname, item.isHome)
: false;

return (
<button
Expand Down
56 changes: 7 additions & 49 deletions app/layouts/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,8 @@ import { IconName } from '@/components/ui/icons/Icon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHouse as faHouseSolid } from '@fortawesome/pro-solid-svg-icons';
import { faHouse as faHouseLight } from '@fortawesome/pro-light-svg-icons';
import { BriefcaseBusiness, Sprout, Star } from 'lucide-react';
import { useOptionalAIMode } from '@/components/AIMode/AIModeContext';
import { AI_MODE_NAME } from '@/components/AIMode/copy';
import { Sprout, Star } from 'lucide-react';
import { isHomeTabPath } from '@/hooks/useFundTabs';
import { useUser } from '@/contexts/UserContext';
import { isHubEditorOrModerator } from '@/utils/permissions';
import { cn } from '@/utils/styles';

interface NavIcon {
Expand All @@ -34,8 +30,6 @@ interface NavigationItem {
isLucideStar?: boolean;
isLucideSprout?: boolean;
isHome?: boolean;
/** Toggles the AI Mode overlay in place instead of navigating. */
isAIMode?: boolean;
}

interface NavigationProps {
Expand Down Expand Up @@ -64,10 +58,6 @@ export const Navigation: React.FC<NavigationProps> = ({
onUnimplementedFeature,
forceMinimize = false,
}) => {
const { user } = useUser();
// The assistant is gated server-side to moderators and hub editors; nobody
// else gets a door to a room they cannot enter.
const canUseAssistant = isHubEditorOrModerator(user);
const navigationItems: NavigationItem[] = [
{
label: 'Home',
Expand Down Expand Up @@ -102,13 +92,6 @@ export const Navigation: React.FC<NavigationProps> = ({
isLucideSprout: true,
description: 'Learn about the ResearchHub Endowment',
},
{
label: AI_MODE_NAME,
href: '#',
isAIMode: true,
requiresAuth: true,
description: 'Draft proposals and RFPs with the assistant',
},
];

const getButtonStyles = (isActive: boolean) => {
Expand Down Expand Up @@ -149,8 +132,7 @@ export const Navigation: React.FC<NavigationProps> = ({
}> = ({ item, onUnimplementedFeature }) => {
const { executeAuthenticatedAction } = useAuthenticatedAction();
const router = useRouter();
const aiMode = useOptionalAIMode();
const isActive = item.isAIMode ? Boolean(aiMode?.isOpen) : isPathActive(item.href, item.isHome);
const isActive = isPathActive(item.href, item.isHome);
const buttonStyles = getButtonStyles(isActive);

const iconColor = isActive ? '#3971ff' : '#404040';
Expand Down Expand Up @@ -187,27 +169,7 @@ export const Navigation: React.FC<NavigationProps> = ({
? 'hidden'
: 'flex w-full min-w-0 items-center tablet:max-sidebar-compact:!hidden';

if (item.isAIMode) {
// Same row as the links, but no navigation: the workspace opens in
// place on its new-conversation screen and the URL only gains a query
// param.
return (
<button
type="button"
onClick={() => executeAuthenticatedAction(() => aiMode?.selectChat(null))}
className={buttonStyles}
aria-pressed={isActive}
title={item.description}
>
<div className={iconContainerClass}>
<BriefcaseBusiness size={22} color={iconColor} strokeWidth={isActive ? 2.25 : 2} />
</div>
<div className={textContainerClass}>
<span className="inline-flex min-w-0 items-center gap-2 truncate">{item.label}</span>
</div>
</button>
);
}
const label = <span className="min-w-0 truncate">{item.label}</span>;

return (
<Link
Expand Down Expand Up @@ -239,9 +201,7 @@ export const Navigation: React.FC<NavigationProps> = ({
<div className="w-[26px] h-[26px]" />
)}
</div>
<div className={textContainerClass}>
<span className="inline-flex min-w-0 items-center gap-2 truncate">{item.label}</span>
</div>
<div className={textContainerClass}>{label}</div>
</Link>
);
};
Expand All @@ -255,11 +215,9 @@ export const Navigation: React.FC<NavigationProps> = ({
)}
>
<div className="space-y-2">
{navigationItems
.filter((item) => !item.isAIMode || canUseAssistant)
.map((item) => (
<NavLink key={item.label} item={item} onUnimplementedFeature={onUnimplementedFeature} />
))}
{navigationItems.map((item) => (
<NavLink key={item.label} item={item} onUnimplementedFeature={onUnimplementedFeature} />
))}
</div>
</nav>
);
Expand Down
11 changes: 11 additions & 0 deletions app/layouts/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,17 @@ const MobileBottomNav = dynamic(
interface PageLayoutProps {
children: ReactNode;
rightSidebar?: boolean | ReactNode;
/** Align a custom sidebar with the main column's padded content. */
rightSidebarTopOffset?: 'default' | 'aligned';
className?: string;
sidebarContentClassName?: string;
topBanner?: ReactNode;
rightSidebarAbove?: ReactNode;
/**
* With a card above it the gray rail normally sizes to its content; this
* keeps it stretched to the bottom of the column regardless.
*/
rightSidebarFill?: boolean;
fundraiseGrantId?: number;
/**
* `default` centers the page in a 1180px container and lets the main column
Expand All @@ -50,10 +57,12 @@ interface PageLayoutProps {
function PageLayoutInner({
children,
rightSidebar = true,
rightSidebarTopOffset = 'default',
className,
sidebarContentClassName,
topBanner,
rightSidebarAbove,
rightSidebarFill = false,
contentWidth = 'default',
wideRow = false,
}: PageLayoutProps) {
Expand Down Expand Up @@ -142,8 +151,10 @@ function PageLayoutInner({
{rightSidebar && (
<RightSidebarContainer
rightSidebar={rightSidebar}
topOffset={rightSidebarTopOffset}
contentClassName={sidebarContentClassName}
aboveSidebar={rightSidebarAbove}
fill={rightSidebarFill}
/>
)}
</div>
Expand Down
66 changes: 43 additions & 23 deletions app/layouts/PublishMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import { ChevronRight, Plus } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBullhorn, faFileSignature } from '@fortawesome/pro-light-svg-icons';
import { BaseMenu, BaseMenuItem } from '@/components/ui/form/BaseMenu';
import { FUNDING_KIND_ICON } from '@/components/Funding/fundingKind';
import { useAuthenticatedAction } from '@/contexts/AuthModalContext';
import { SwipeableDrawer } from '@/components/ui/SwipeableDrawer';
import {
Expand All @@ -15,36 +15,58 @@ import {
OpenProposalModal,
type ProposalCreationMethod,
} from '@/components/Funding/OpenProposalModal';
import type { FundingIntent } from '@/components/Funding/fundingDirection';
import { useFundingDrafting } from '@/components/Funding/useFundingDrafting';
import { useScreenSize } from '@/hooks/useScreenSize';
import { useState } from 'react';

interface PublishMenuProps {
forceMinimize?: boolean;
}

const PUBLISH_MENU_SECTIONS = [
interface PublishMenuItem {
readonly id: string;
readonly title: string;
readonly description: string;
readonly icon: React.ReactNode;
/** Which side of the money the item starts: an RFP funds, a proposal needs funding. */
readonly intent: FundingIntent;
}

const PUBLISH_MENU_SECTIONS: readonly {
readonly title: string;
readonly items: readonly PublishMenuItem[];
}[] = [
{
title: 'Publish on ResearchHub',
items: [
{
id: 'give-funding',
title: 'Request for Proposal',
description: 'Fund specific research you care about',
icon: <FontAwesomeIcon icon={faBullhorn} className="h-[18px] w-[18px] text-gray-700" />,
handler: 'handleOpenGrant',
icon: (
<FontAwesomeIcon
icon={FUNDING_KIND_ICON.rfp}
className="h-[18px] w-[18px] text-gray-700"
/>
),
intent: 'fund',
},
{
id: 'request-funding',
title: 'Proposal',
description: 'Raise money for your research',
icon: (
<FontAwesomeIcon icon={faFileSignature} className="h-[18px] w-[18px] text-gray-700" />
<FontAwesomeIcon
icon={FUNDING_KIND_ICON.proposal}
className="h-[18px] w-[18px] text-gray-700"
/>
),
handler: 'handleFundResearch',
intent: 'need_funding',
},
],
},
] as const;
];

interface MenuItemContentProps {
icon: React.ReactNode;
Expand All @@ -69,41 +91,39 @@ const MenuItemContent: React.FC<MenuItemContentProps> = ({ icon, title, descript
);
};

/**
* The sidebar's Publish button: a Request for Proposal or a Proposal. For a
* user the workspace admits, either opens it straight onto a conversation
* for that side of the money; everyone else gets the notebook's opening
* modal and editor, as before.
*/
export const PublishMenu: React.FC<PublishMenuProps> = ({ forceMinimize = false }) => {
const router = useRouter();
const { executeAuthenticatedAction } = useAuthenticatedAction();
const { inWorkspace, startNew } = useFundingDrafting();
const { smAndDown } = useScreenSize();
const [isMobileDrawerOpen, setIsMobileDrawerOpen] = useState(false);
const [isFundingOpportunityModalOpen, setIsFundingOpportunityModalOpen] = useState(false);
const [isProposalModalOpen, setIsProposalModalOpen] = useState(false);

const handleFundResearch = () => {
setIsProposalModalOpen(true);
};

const handleConfirmCreateProposal = (method: ProposalCreationMethod) => {
setIsProposalModalOpen(false);
router.push(`/notebook?newFunding=true&proposalSource=${method}`);
};

const handleOpenGrant = () => {
setIsFundingOpportunityModalOpen(true);
};

const handleConfirmOpenGrant = (method: FundingOpportunityCreationMethod) => {
setIsFundingOpportunityModalOpen(false);
router.push(`/notebook?newGrant=true&grantSource=${method}`);
};

const handleMenuItemClick = (item: (typeof PUBLISH_MENU_SECTIONS)[number]['items'][number]) => {
const handleMenuItemClick = (item: PublishMenuItem) => {
executeAuthenticatedAction(() => {
switch (item.handler) {
case 'handleFundResearch':
handleFundResearch();
break;
case 'handleOpenGrant':
handleOpenGrant();
break;
if (inWorkspace) {
startNew(item.intent);
} else if (item.intent === 'fund') {
setIsFundingOpportunityModalOpen(true);
} else {
setIsProposalModalOpen(true);
}
});

Expand Down
Loading
Loading