Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/web/src/components/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function Hero() {
real-time collaboration, and automation that improve transparency and efficiency at
every stage.
</p>
<div className='mb-2 flex flex-col justify-center gap-4 sm:flex-row sm:gap-12'>
<div className='mb-2 flex flex-col justify-center gap-4 sm:flex-row sm:gap-6'>
<Link
to='/checklist'
className='inline-flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-5 py-2.5 text-base font-semibold text-white shadow-md shadow-blue-600/20 transition-colors hover:bg-blue-500 sm:w-auto'
Expand All @@ -29,7 +29,7 @@ export default function Hero() {
</Link>
<Link
to='/signup'
className='inline-flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-5 py-2.5 text-base font-semibold text-white shadow-md shadow-blue-600/20 transition-colors hover:bg-blue-500 sm:w-auto'
className='inline-flex w-full items-center justify-center gap-2 rounded-lg border-2 border-blue-600 px-5 py-2.5 text-base font-semibold text-blue-600 transition-colors hover:bg-blue-50 sm:w-auto'
>
Start a Review Project
</Link>
Expand Down
12 changes: 5 additions & 7 deletions packages/web/src/components/dashboard/ContactPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ interface ContactPromptProps {

export function ContactPrompt({ restrictionType, projectCount, quotaLimit }: ContactPromptProps) {
const title =
restrictionType === 'entitlement' ? 'Early Access Testing' : 'Project Limit Reached';
restrictionType === 'entitlement' ? 'Ready to Collaborate?' : 'Project Limit Reached';

const message =
restrictionType === 'entitlement' ?
"CoRATES is currently in early access testing as we test and refine project-based features in collaboration with early users. If you're interested in creating a project and sharing feedback, please contact us to request access. Individual appraisals are always available and free to use."
: `You've reached your project limit (${projectCount}/${quotaLimit === null || quotaLimit === -1 ? 'unlimited' : quotaLimit}). Request early access for more projects.`;
'Projects let your team appraise studies independently and resolve disagreements together. Start a 14-day free trial to create your first project -- no credit card required.'
: `You've reached your project limit (${projectCount}/${quotaLimit === null || quotaLimit === -1 ? 'unlimited' : quotaLimit}). Upgrade your plan to create more projects.`;

return (
<div className='bg-info-bg border-info-border flex items-center justify-between rounded-lg border p-4'>
Expand All @@ -24,12 +24,10 @@ export function ContactPrompt({ restrictionType, projectCount, quotaLimit }: Con
<p className='text-muted-foreground text-sm'>{message}</p>
</div>
<a
href='/contact'
target='_blank'
rel='noopener noreferrer'
href='/pricing'
className='bg-primary hover:bg-primary/90 ml-4 rounded-lg px-4 py-2 font-medium whitespace-nowrap text-white transition-colors focus:ring-2 focus:ring-blue-500 focus:outline-none'
>
Contact Us
View Plans
</a>
</div>
);
Expand Down
25 changes: 24 additions & 1 deletion packages/web/src/components/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ import { useSubscription } from '@/hooks/useSubscription';
import { Alert } from '@/components/ui/alert';

import { DashboardHeader } from './DashboardHeader';
import { WelcomeCard } from './WelcomeCard';
import { ProjectsSection } from './ProjectsSection';
import { LocalAppraisalsSection } from './LocalAppraisalsSection';
import { useInitialAnimation, AnimationContext } from './useInitialAnimation';

const WELCOME_DISMISSED_KEY = 'corates-welcome-dismissed';

export function Dashboard() {
const animation = useInitialAnimation();

Expand All @@ -23,6 +26,24 @@ export function Dashboard() {
const { subscriptionFetchFailed } = useSubscription();

const [createModalOpen, setCreateModalOpen] = useState(false);
const [welcomeDismissed, setWelcomeDismissed] = useState(() => {
try {
return localStorage.getItem(WELCOME_DISMISSED_KEY) === 'true';
} catch {
return false;
}
});

const dismissWelcome = () => {
try {
localStorage.setItem(WELCOME_DISMISSED_KEY, 'true');
} catch {
// localStorage unavailable
}
setWelcomeDismissed(true);
};

const showWelcomeCard = isLoggedIn && !welcomeDismissed;

return (
<AnimationContext.Provider value={animation}>
Expand All @@ -33,7 +54,9 @@ export function Dashboard() {
</Alert>
)}

<DashboardHeader user={user} />
{showWelcomeCard ?
<WelcomeCard user={user!} onDismiss={dismissWelcome} />
: <DashboardHeader user={user} />}

<div id='projects-section' className='flex flex-col gap-8'>
{isLoggedIn && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,9 @@ export function LocalAppraisalsSection({
<h3 className='text-secondary-foreground mb-1 text-sm font-medium'>
No local appraisals
</h3>
<p className='text-muted-foreground mb-4 text-center text-xs'>
Create appraisals that stay on this device
<p className='text-muted-foreground mb-4 max-w-sm text-center text-xs'>
Use AMSTAR 2, ROBINS-I, or RoB 2 to appraise studies on this device with optional PDF
annotation
</p>
<button
type='button'
Expand Down
93 changes: 38 additions & 55 deletions packages/web/src/components/dashboard/QuickActions.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,60 @@
/**
* QuickActions - Quick start cards for creating new appraisals
* QuickActions - Quick start cards for creating new appraisals by type
*/

import { PlayCircleIcon, BookOpenIcon } from 'lucide-react';
import { useAnimation } from './useInitialAnimation';

interface QuickActionsProps {
onStartROBINSI: () => void;
onStartAMSTAR2: () => void;
onLearnMore: () => void;
}
import { useNavigate } from '@tanstack/react-router';
import { PlayCircleIcon } from 'lucide-react';

const ACTIONS = [
{
id: 'robins-i',
title: 'Start ROBINS-I',
description: 'Risk of bias for non-randomized studies',
icon: <PlayCircleIcon className='size-6 text-blue-600' />,
type: 'AMSTAR2',
title: 'AMSTAR 2',
description: 'Quality assessment of systematic reviews',
iconColor: 'text-blue-600',
iconBg: 'bg-blue-50',
border: 'border-blue-100 hover:border-blue-200',
},
{
id: 'amstar-2',
title: 'Start AMSTAR 2',
description: 'Quality assessment for systematic reviews',
icon: <PlayCircleIcon className='text-success size-6' />,
iconBg: 'bg-success-bg',
border: 'border-success-border/50 hover:border-success-border',
type: 'ROBINS_I',
title: 'ROBINS-I',
description: 'Risk of bias in non-randomized studies',
iconColor: 'text-emerald-600',
iconBg: 'bg-emerald-50',
border: 'border-emerald-100 hover:border-emerald-200',
},
{
id: 'learn-more',
title: 'Learn More',
description: 'View documentation and guides',
icon: <BookOpenIcon className='size-6 text-violet-600' />,
type: 'ROB2',
title: 'RoB 2',
description: 'Risk of bias in randomized trials',
iconColor: 'text-violet-600',
iconBg: 'bg-violet-50',
border: 'border-violet-100 hover:border-violet-200',
},
];

export function QuickActions({ onStartROBINSI, onStartAMSTAR2, onLearnMore }: QuickActionsProps) {
const animation = useAnimation();

const handlers: Record<string, () => void> = {
'robins-i': onStartROBINSI,
'amstar-2': onStartAMSTAR2,
'learn-more': onLearnMore,
};
export function QuickActions() {
const navigate = useNavigate();

return (
<section className='mb-6' style={animation.fadeUp(400)}>
<h3 className='text-muted-foreground mb-4 text-sm font-semibold tracking-wide uppercase'>
Quick Start
</h3>
<div className='grid gap-3'>
{ACTIONS.map(action => (
<button
key={action.id}
type='button'
onClick={handlers[action.id]}
className={`group bg-card flex items-center gap-4 rounded-xl border p-4 text-left transition-all duration-200 hover:shadow-md ${action.border}`}
<div className='grid gap-3 sm:grid-cols-3'>
{ACTIONS.map(action => (
<button
key={action.type}
type='button'
onClick={() => navigate({ to: '/checklist' as string, search: { type: action.type } })}
className={`group bg-card flex items-center gap-3 rounded-xl border p-4 text-left transition-all duration-200 hover:shadow-md ${action.border}`}
>
<div
className={`flex size-10 shrink-0 items-center justify-center rounded-xl ${action.iconBg} transition-transform duration-200 group-hover:scale-105`}
>
<div
className={`flex size-12 shrink-0 items-center justify-center rounded-xl ${action.iconBg} transition-transform duration-200 group-hover:scale-105`}
>
{action.icon}
</div>
<div>
<h4 className='text-foreground font-medium'>{action.title}</h4>
<p className='text-muted-foreground text-sm'>{action.description}</p>
</div>
</button>
))}
</div>
</section>
<PlayCircleIcon className={`size-5 ${action.iconColor}`} />
</div>
<div className='min-w-0'>
<h4 className='text-foreground text-sm font-medium'>{action.title}</h4>
<p className='text-muted-foreground text-xs leading-tight'>{action.description}</p>
</div>
</button>
))}
</div>
);
}
60 changes: 60 additions & 0 deletions packages/web/src/components/dashboard/WelcomeCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* WelcomeCard - First-time user onboarding with quick actions
*
* Shown on the dashboard for logged-in users who haven't dismissed it.
* Replaces the standard DashboardHeader greeting until dismissed.
*/

import { Link } from '@tanstack/react-router';
import { XIcon } from 'lucide-react';
import type { AuthUser } from '@/stores/authStore';
import { useAnimation } from './useInitialAnimation';
import { QuickActions } from './QuickActions';

interface WelcomeCardProps {
user: AuthUser;
onDismiss: () => void;
}

export function WelcomeCard({ user, onDismiss }: WelcomeCardProps) {
const animation = useAnimation();
const firstName = user.givenName || user.name || '';

return (
<section className='mb-8' style={animation.fadeUp(0)}>
<div className='border-border bg-card relative rounded-2xl border p-6 sm:p-8'>
<button
type='button'
onClick={onDismiss}
className='text-muted-foreground hover:text-foreground absolute top-4 right-4 rounded-lg p-1 transition-colors'
aria-label='Dismiss welcome card'
>
<XIcon className='size-5' />
</button>

<div className='mb-6 pr-8'>
<h1 className='text-foreground text-2xl font-semibold tracking-tight sm:text-3xl'>
Welcome{firstName ? `, ${firstName}` : ''}
</h1>
<p className='text-muted-foreground mt-2 max-w-xl'>
Start appraising studies right away. Pick a tool below to create your first appraisal.
</p>
</div>

<QuickActions />

<div className='border-border mt-6 border-t pt-4'>
<p className='text-muted-foreground text-sm'>
<span className='text-foreground font-medium'>Local appraisals</span> are saved on this
device and always free. <span className='text-foreground font-medium'>Projects</span>{' '}
let teams appraise independently and resolve disagreements together.{' '}
<Link to='/pricing' className='text-primary hover:text-primary/80'>
Explore plans
</Link>{' '}
when you&#39;re ready to collaborate.
</p>
</div>
</div>
</section>
);
}
Loading