From b4136555fa70907d48cc199459c9892aea2b130e Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Fri, 9 Jan 2026 17:41:34 -0600 Subject: [PATCH 01/15] add mocks --- packages/docs/plans/dashboard-redesign.md | 528 +++++++++++++++ packages/web/src/Routes.jsx | 11 +- .../src/components/mocks/DashboardMock.jsx | 609 ++++++++++++++++++ .../web/src/components/mocks/MockIndex.jsx | 142 +++- .../components/mocks/SettingsMockBento.jsx | 489 ++++++++++++++ .../components/mocks/SettingsMockMinimal.jsx | 543 ++++++++++++++++ 6 files changed, 2289 insertions(+), 33 deletions(-) create mode 100644 packages/docs/plans/dashboard-redesign.md create mode 100644 packages/web/src/components/mocks/DashboardMock.jsx create mode 100644 packages/web/src/components/mocks/SettingsMockBento.jsx create mode 100644 packages/web/src/components/mocks/SettingsMockMinimal.jsx diff --git a/packages/docs/plans/dashboard-redesign.md b/packages/docs/plans/dashboard-redesign.md new file mode 100644 index 000000000..5797d74db --- /dev/null +++ b/packages/docs/plans/dashboard-redesign.md @@ -0,0 +1,528 @@ +# Dashboard Redesign Plan + +This plan outlines the implementation of a redesigned dashboard based on the mock at `/mocks/dashboard`. The goal is to replace the existing minimal dashboard with a production-grade experience that handles all user states gracefully. + +## Current State Analysis + +### Existing Implementation + +The current dashboard ([Dashboard.jsx](packages/web/src/components/Dashboard.jsx)) is minimal: + +```jsx +// Current structure (simplified) + + + + +``` + +### Key Components to Replace/Integrate + +| Component | Location | Purpose | +| ---------------------- | ------------------------------------ | --------------------------------- | +| `ProjectsPanel` | `project/ProjectsPanel.jsx` | Projects grid with create form | +| `LocalAppraisalsPanel` | `checklist/LocalAppraisalsPanel.jsx` | Device-local appraisals | +| `ProjectCard` | `project/ProjectCard.jsx` | Individual project card | +| `ContactPrompt` | `project/ContactPrompt.jsx` | Early access / quota limit prompt | +| `CreateProjectForm` | `project/CreateProjectForm.jsx` | Project creation modal | + +### State Dependencies + +| State | Source | Used For | +| --------------------------- | ----------------- | --------------------------- | +| `isLoggedIn()` | `useBetterAuth` | Show projects section | +| `authLoading()` | `useBetterAuth` | Loading skeleton | +| `isOnline()` | `useBetterAuth` | Disable create when offline | +| `hasEntitlement()` | `useSubscription` | Project creation permission | +| `hasQuota()` | `useSubscription` | Project limit check | +| `subscriptionLoading()` | `useSubscription` | Loading states | +| `subscriptionFetchFailed()` | `useSubscription` | Error banner | + +--- + +## User State Matrix + +The dashboard must handle these distinct states: + +| # | State | Auth | Subscription | UI Requirements | +| --- | ----------------------- | -------- | ------------ | -------------------------------------------------- | +| 1 | **Logged Out** | No | N/A | Local appraisals only, prominent sign-in CTA | +| 2 | **Loading** | Checking | Checking | Skeleton UI, no flickering | +| 3 | **Free (No Plan)** | Yes | None/Free | Show projects (if any), ContactPrompt for creation | +| 4 | **Trial/Grant** | Yes | Trial | Full features, show trial status | +| 5 | **Active Subscriber** | Yes | Active | Full features, usage stats | +| 6 | **Quota Exceeded** | Yes | Active | Show projects, ContactPrompt for more | +| 7 | **Subscription Error** | Yes | Failed | Warning banner, retry button | +| 8 | **Offline (Logged In)** | Cached | Cached | Read-only mode, offline indicator | + +--- + +## Implementation Phases + +### Phase 1: Core Layout and Components + +**Goal:** Implement the new dashboard structure with proper state handling. + +#### 1.1 Create New Dashboard Component + +**File:** `packages/web/src/components/dashboard/Dashboard.jsx` + +Create a new dashboard module directory with: + +``` +components/dashboard/ + Dashboard.jsx # Main container with state logic + DashboardHeader.jsx # Welcome section with user info + StatsRow.jsx # Stats cards row + ProjectsSection.jsx # Projects grid + LocalSection.jsx # Local appraisals section + ActivityFeed.jsx # Recent activity sidebar + ProgressCard.jsx # Overall progress visualization + QuickActions.jsx # Quick start actions + index.js # Barrel export +``` + +#### 1.2 Dashboard State Machine + +```jsx +// Pseudo-code for state handling +const dashboardState = createMemo(() => { + if (authLoading()) return 'loading'; + if (!isLoggedIn()) return 'logged-out'; + if (subscriptionLoading()) return 'loading-subscription'; + if (subscriptionFetchFailed()) return 'subscription-error'; + if (!hasEntitlement('project.create')) return 'no-plan'; + if (!hasQuota('projects.max', { used: projectCount(), requested: 1 })) return 'quota-exceeded'; + return 'active'; +}); +``` + +#### 1.3 Tasks + +- [ ] Create `dashboard/` directory structure +- [ ] Implement `DashboardHeader` with user greeting and date +- [ ] Implement `StatsRow` with computed stats from real data +- [ ] Implement `ProgressCard` with SVG arc visualization +- [ ] Implement `QuickActions` with navigation to appraisal creation +- [ ] Implement `ActivityFeed` (initially with placeholder data) +- [ ] Update main `Dashboard.jsx` to use new components + +--- + +### Phase 2: Projects Integration + +**Goal:** Integrate real project data with the new visual design. + +#### 2.1 New ProjectCard Component + +The mock's `ProjectCard` needs to be reconciled with the existing one. Create a new enhanced version: + +**File:** `packages/web/src/components/dashboard/ProjectCard.jsx` + +Features: + +- Accent color based on project index or hash +- Progress bar with gradient +- Role badge (Lead/Reviewer) +- Member count +- Relative timestamp +- Hover effects and animations + +#### 2.2 Projects Grid Integration + +```jsx +// Key integration points +- Use `useMyProjectsList()` for project data +- Use `useSubscription()` for quota checks +- Handle empty state with create prompt +- Handle offline state (disable creation) +- Show `ContactPrompt` when quota/entitlement blocked +``` + +#### 2.3 Tasks + +- [ ] Create enhanced `ProjectCard` component +- [ ] Implement project color assignment (deterministic hash) +- [ ] Add progress calculation (completed/total studies) +- [ ] Integrate delete confirmation dialog +- [ ] Wire up navigation on card click +- [ ] Handle empty project state + +--- + +### Phase 3: Local Appraisals Integration + +**Goal:** Integrate local appraisals with improved visual design. + +#### 3.1 LocalAppraisalCard Component + +**File:** `packages/web/src/components/dashboard/LocalAppraisalCard.jsx` + +Features: + +- Compact horizontal layout +- Checklist type badge +- Relative timestamp +- Delete action +- Inline rename (using Editable) + +#### 3.2 Tasks + +- [ ] Create `LocalAppraisalCard` component +- [ ] Use `localChecklistsStore` for data +- [ ] Implement delete with confirmation +- [ ] Add inline rename functionality +- [ ] Handle empty state + +--- + +### Phase 4: Authentication States + +**Goal:** Handle logged-out and loading states gracefully. + +#### 4.1 Logged Out State + +When `!isLoggedIn()`: + +``` ++------------------------------------------+ +| Welcome to CoRATES | +| Evidence synthesis made collaborative | +| | +| [Sign In] [Create Free Account] | ++------------------------------------------+ +| | +| Your Local Appraisals | +| (device-stored, no account needed) | +| | +| [Local appraisal cards...] | +| | +| Want to collaborate? | +| [Sign in to sync and share] | ++------------------------------------------+ +``` + +#### 4.2 Loading State + +Skeleton UI matching the final layout: + +- Pulsing placeholder for header +- Skeleton cards for stats +- Ghost cards for projects +- Prevent layout shift + +#### 4.3 Tasks + +- [ ] Create `LoggedOutHero` component +- [ ] Create `DashboardSkeleton` component +- [ ] Ensure smooth transition from loading to loaded +- [ ] Test with slow network conditions + +--- + +### Phase 5: Subscription States + +**Goal:** Handle subscription edge cases with appropriate UI. + +#### 5.1 No Plan / Free Tier + +Show limited dashboard with `ContactPrompt`: + +```jsx + + + +``` + +- Projects section shows existing projects (if any) +- Create button replaced with ContactPrompt +- Full access to local appraisals + +#### 5.2 Quota Exceeded + +Similar to no plan, but different messaging: + +- "You've reached your project limit (3/3)" +- Contact for more capacity + +#### 5.3 Subscription Error + +Warning banner with retry: + +```jsx + +
+ Unable to verify subscription. Some features may be restricted. + +
+
+``` + +#### 5.4 Tasks + +- [ ] Integrate `ContactPrompt` into new layout +- [ ] Style subscription error banner +- [ ] Add subscription status to header (if trial/expiring) +- [ ] Test all subscription states + +--- + +### Phase 6: Activity and Stats + +**Goal:** Add real activity data and computed statistics. + +#### 6.1 Stats Computation + +```jsx +const stats = createMemo(() => ({ + activeProjects: projects()?.length || 0, + studiesReviewed: projects()?.reduce((sum, p) => sum + (p.completedCount || 0), 0) || 0, + totalStudies: projects()?.reduce((sum, p) => sum + (p.studyCount || 0), 0) || 0, + localAppraisals: checklists()?.length || 0, + teamMembers: computeUniqueTeamMembers(projects()), +})); +``` + +#### 6.2 Activity Feed + +Initial implementation with local-only activity: + +- Track checklist opens/updates in local storage +- Track project opens +- Future: Real activity from API + +#### 6.3 Tasks + +- [ ] Add `studyCount` and `completedCount` to project list API +- [ ] Implement stats computation +- [ ] Create activity tracking utilities +- [ ] Wire up activity feed + +--- + +### Phase 7: Polish and Animations + +**Goal:** Add the refined animations and transitions from the mock. + +#### 7.1 Animation System + +```css +@keyframes card-rise { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes stat-rise { + from { + opacity: 0; + transform: translateY(8px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +``` + +#### 7.2 Staggered Loading + +Cards appear with staggered delays based on index: + +```jsx + + {(project, index) => ( +
+ +
+ )} +
+``` + +#### 7.3 Tasks + +- [ ] Extract animations to shared CSS file +- [ ] Implement staggered card loading +- [ ] Add hover effects to all interactive elements +- [ ] Ensure animations respect `prefers-reduced-motion` + +--- + +## File Changes Summary + +### New Files + +``` +packages/web/src/components/dashboard/ + Dashboard.jsx + DashboardHeader.jsx + DashboardSkeleton.jsx + LoggedOutHero.jsx + StatsRow.jsx + StatCard.jsx + ProjectsSection.jsx + ProjectCard.jsx + LocalSection.jsx + LocalAppraisalCard.jsx + ActivityFeed.jsx + ProgressCard.jsx + QuickActions.jsx + CollaborationCTA.jsx + index.js +``` + +### Modified Files + +``` +packages/web/src/Routes.jsx # Update Dashboard import +packages/web/src/global.css # Add dashboard animations +packages/workers/src/routes/orgs/projects.js # Add studyCount to response +``` + +### Deleted Files + +``` +packages/web/src/components/Dashboard.jsx # Replaced by dashboard/Dashboard.jsx +``` + +--- + +## API Changes Required + +### Project List Response Enhancement + +Current response: + +```json +{ + "id": "...", + "name": "...", + "description": "...", + "role": "owner|reviewer", + "createdAt": "..." +} +``` + +Enhanced response: + +```json +{ + "id": "...", + "name": "...", + "description": "...", + "role": "owner|reviewer", + "createdAt": "...", + "studyCount": 24, + "completedCount": 16, + "memberCount": 4, + "lastActivity": "2026-01-09T..." +} +``` + +--- + +## Testing Requirements + +### Unit Tests + +- [ ] `DashboardHeader` renders user name correctly +- [ ] `StatsRow` computes stats from project data +- [ ] `ProgressCard` calculates percentage correctly +- [ ] `ProjectCard` handles missing data gracefully +- [ ] State machine returns correct state for each scenario + +### Integration Tests + +- [ ] Logged out user sees local appraisals only +- [ ] Loading state shows skeleton +- [ ] Projects load and display correctly +- [ ] Create project button disabled when offline +- [ ] ContactPrompt shows for free tier users +- [ ] Subscription error banner shows with retry + +### Visual Tests + +- [ ] Animations play correctly +- [ ] Responsive layout at mobile/tablet/desktop +- [ ] Dark mode (if applicable) +- [ ] Color contrast meets WCAG AA + +--- + +## Migration Strategy + +### Step 1: Feature Flag + +Add a feature flag to toggle between old and new dashboard: + +```jsx +const useNewDashboard = () => localStorage.getItem('corates-new-dashboard') === 'true'; +``` + +### Step 2: Parallel Development + +Keep both dashboards working during development. Route to new dashboard when flag is enabled. + +### Step 3: Gradual Rollout + +1. Internal testing with flag enabled +2. Enable for early access users +3. A/B test if metrics available +4. Full rollout +5. Remove old dashboard + +--- + +## Success Metrics + +- **Visual:** Dashboard matches mock design within 95% fidelity +- **Performance:** First contentful paint under 500ms +- **UX:** All user states handled without blank screens or errors +- **Accessibility:** Keyboard navigable, screen reader friendly +- **Code Quality:** Components under 200 lines, proper separation of concerns + +--- + +## Timeline Estimate + +| Phase | Effort | Dependencies | +| ---------------------------- | -------- | ------------ | +| Phase 1: Core Layout | 2-3 days | None | +| Phase 2: Projects | 1-2 days | Phase 1 | +| Phase 3: Local Appraisals | 1 day | Phase 1 | +| Phase 4: Auth States | 1 day | Phase 1 | +| Phase 5: Subscription States | 1 day | Phase 2 | +| Phase 6: Activity & Stats | 1-2 days | API changes | +| Phase 7: Polish | 1 day | All phases | + +**Total: 8-12 days** + +--- + +## Open Questions + +1. **Activity Feed Data:** Should we implement a real activity API, or start with local-only tracking? + +2. **Color Assignment:** Should project colors be user-configurable, or deterministically assigned? + +3. **Mobile Layout:** The mock is desktop-focused. Do we need a separate mobile design, or responsive adaptation? + +4. **Search:** The mock shows a search button. Should we implement global search now or defer? + +5. **Dark Mode:** Should the new dashboard support dark mode from launch? diff --git a/packages/web/src/Routes.jsx b/packages/web/src/Routes.jsx index 29fc13484..dedf6e0df 100644 --- a/packages/web/src/Routes.jsx +++ b/packages/web/src/Routes.jsx @@ -37,7 +37,10 @@ const ProjectViewComplete = lazy(() => import('@/components/mocks/ProjectViewCom const AddStudiesWizard = lazy(() => import('@/components/mocks/AddStudiesWizard.jsx')); const AddStudiesPanel = lazy(() => import('@/components/mocks/AddStudiesPanel.jsx')); const AddStudiesInline = lazy(() => import('@/components/mocks/AddStudiesInline.jsx')); - +const SettingsMockBento = lazy(() => import('@/components/mocks/SettingsMockBento.jsx')); +const SettingsMockMinimal = lazy(() => import('@/components/mocks/SettingsMockMinimal.jsx')); +const DashboardMock = lazy(() => import('@/components/mocks/DashboardMock.jsx')); +(''); // Code-split admin routes - loaded only when navigating to /admin/* const AdminDashboard = lazy(() => import('@/components/admin/index.js').then(m => ({ default: m.AdminDashboard })), @@ -76,7 +79,6 @@ export default function AppRoutes() { {/* Dashboard - public home for all users */} - {/* Protected routes - requires login */} {/* Global user routes (no sidebar) */} @@ -119,11 +121,9 @@ export default function AppRoutes() { /> - {/* Local checklists (not org-scoped, work offline) */} - {/* Mock routes - public, visual-only wireframes */} @@ -133,6 +133,9 @@ export default function AppRoutes() { + + + diff --git a/packages/web/src/components/mocks/DashboardMock.jsx b/packages/web/src/components/mocks/DashboardMock.jsx new file mode 100644 index 000000000..b5aa23aac --- /dev/null +++ b/packages/web/src/components/mocks/DashboardMock.jsx @@ -0,0 +1,609 @@ +/** + * Dashboard Mock - Clean Modern Style + * + * Design Direction: Clean, professional research dashboard. + * A refined aesthetic with: + * - Inter font family for clarity and readability + * - Blue brand accents with warm stone neutrals + * - Paper-like textures and layered cards + * - Clear data hierarchy and beautiful progress visualization + * - Generous whitespace with intentional asymmetry + * + * Inspired by: Linear's precision, Notion's warmth + */ + +import { For, Show, createSignal, createEffect, onMount } from 'solid-js'; +import { + FiPlus, + FiFolder, + FiUsers, + FiClock, + FiCheck, + FiArrowRight, + FiFileText, + FiTrendingUp, + FiChevronRight, + FiSearch, + FiBook, + FiBarChart2, +} from 'solid-icons/fi'; +import { BiRegularBookmark } from 'solid-icons/bi'; +import { AiOutlineExperiment } from 'solid-icons/ai'; +import { VsBeaker } from 'solid-icons/vs'; + +// Mock Data +const mockUser = { + name: 'Dr. Sarah Chen', + institution: 'Stanford School of Medicine', + avatar: null, +}; + +const mockProjects = [ + { + id: '1', + name: 'Mindfulness Interventions for Chronic Pain', + description: 'Systematic review of RCTs examining mindfulness-based interventions', + role: 'owner', + studies: 24, + completed: 16, + members: 4, + updatedAt: '2 hours ago', + color: 'blue', + }, + { + id: '2', + name: 'Digital Therapeutics for Anxiety Disorders', + description: 'Meta-analysis of mobile app interventions for GAD', + role: 'reviewer', + studies: 18, + completed: 8, + members: 3, + updatedAt: '1 day ago', + color: 'amber', + }, + { + id: '3', + name: 'Exercise & Cognitive Function in Aging', + description: 'Umbrella review of physical activity interventions', + role: 'reviewer', + studies: 42, + completed: 38, + members: 6, + updatedAt: '3 days ago', + color: 'rose', + }, +]; + +const mockLocalAppraisals = [ + { + id: 'local-1', + name: 'MBSR RCT - Johnson 2023', + type: 'ROBINS-I', + updatedAt: 'Today', + }, + { + id: 'local-2', + name: 'CBT Meta-analysis Quality Check', + type: 'AMSTAR 2', + updatedAt: 'Yesterday', + }, +]; + +const mockActivity = [ + { + action: 'completed', + project: 'Mindfulness Interventions', + study: 'MBSR for Low Back Pain', + time: '2h ago', + }, + { + action: 'started', + project: 'Digital Therapeutics', + study: 'Calm App RCT', + time: '5h ago', + }, + { + action: 'reconciled', + project: 'Exercise & Cognitive', + study: 'Walking Intervention Study', + time: '1d ago', + }, +]; + +// Accent color map +const accentColors = { + blue: { + bg: 'bg-blue-50', + border: 'border-blue-200', + text: 'text-blue-700', + ring: 'ring-blue-500/20', + fill: 'bg-blue-500', + gradient: 'from-blue-500 to-blue-600', + }, + amber: { + bg: 'bg-amber-50', + border: 'border-amber-200', + text: 'text-amber-700', + ring: 'ring-amber-500/20', + fill: 'bg-amber-500', + gradient: 'from-amber-400 to-orange-500', + }, + rose: { + bg: 'bg-rose-50', + border: 'border-rose-200', + text: 'text-rose-700', + ring: 'ring-rose-500/20', + fill: 'bg-rose-500', + gradient: 'from-rose-400 to-pink-500', + }, +}; + +// Progress Arc component +function ProgressArc(props) { + const percentage = () => Math.round((props.completed / props.total) * 100); + const circumference = 2 * Math.PI * 36; + const offset = () => circumference - (percentage() / 100) * circumference; + + return ( +
+ + + + + + + + + + +
+ {percentage()}% +
+
+ ); +} + +// Project Card component +function ProjectCard(props) { + const colors = () => accentColors[props.project.color] || accentColors.blue; + const percentage = () => Math.round((props.project.completed / props.project.studies) * 100) || 0; + + return ( +
+ {/* Decorative corner accent */} +
+ + {/* Header */} +
+
+
+ + {props.project.role === 'owner' ? 'Lead' : 'Reviewer'} + + {props.project.updatedAt} +
+

+ {props.project.name} +

+
+
+ + {/* Description */} +

+ {props.project.description} +

+ + {/* Progress bar */} +
+
+ Progress + + {props.project.completed}/{props.project.studies} studies + +
+
+
+
+
+ + {/* Footer */} +
+
+ + {props.project.members} members +
+ +
+
+ ); +} + +// Local Appraisal Card +function LocalAppraisalCard(props) { + return ( +
+
+ +
+
+

{props.appraisal.name}

+
+ {props.appraisal.type} + {props.appraisal.updatedAt} +
+
+ +
+ ); +} + +// Activity Item +function ActivityItem(props) { + const actionColors = { + completed: 'bg-emerald-500', + started: 'bg-blue-500', + reconciled: 'bg-amber-500', + }; + + return ( +
+
+
+

+ {props.activity.action}{' '} + appraisal in{' '} + {props.activity.project} +

+

{props.activity.study}

+
+ {props.activity.time} +
+ ); +} + +// Stat Card +function StatCard(props) { + return ( +
+
+
+

{props.label}

+

{props.value}

+
+
+ {props.icon} +
+
+ +

{props.subtext}

+
+
+ ); +} + +// Main Dashboard Component +export default function DashboardMock() { + const [mounted, setMounted] = createSignal(false); + + onMount(() => { + requestAnimationFrame(() => setMounted(true)); + }); + + const totalStudies = () => mockProjects.reduce((sum, p) => sum + p.studies, 0); + const completedStudies = () => mockProjects.reduce((sum, p) => sum + p.completed, 0); + + return ( +
+ {/* Google Fonts */} + + + + + + +
+ {/* Subtle dot pattern background */} +
+ + {/* Main Content */} +
+ {/* Header */} +
+
+
+

Welcome back,

+

+ {mockUser.name} +

+

{mockUser.institution}

+
+
+ + +
+
+
+ + {/* Stats Row */} +
+ } + iconBg='bg-blue-50' + delay={0} + /> + } + iconBg='bg-emerald-50' + delay={50} + /> + } + iconBg='bg-amber-50' + delay={100} + /> + } + iconBg='bg-violet-50' + delay={150} + /> +
+ + {/* Main Grid */} +
+ {/* Left Column - Projects */} +
+ {/* Projects Section */} +
+
+
+

Your Projects

+

Systematic reviews and meta-analyses

+
+ +
+ +
+ + {(project, index) => ( +
+ +
+ )} +
+
+
+ + {/* Local Appraisals Section */} +
+
+
+

Local Appraisals

+

Saved on this device

+
+ +
+ +
+ + {appraisal => } + +
+
+
+ + {/* Right Column - Activity & Quick Actions */} +
+ {/* Overall Progress Card */} +
+

+ Overall Progress +

+
+ +
+
+

+ {completedStudies()} + / {totalStudies()} studies completed +

+
+
+ + {/* Recent Activity */} +
+

+ Recent Activity +

+
+ {activity => } +
+
+ + {/* Quick Actions */} +
+

+ Quick Start +

+
+ + + +
+
+ + {/* Collaboration Prompt */} +
+
+ +
+

Invite Collaborators

+

+ Add team members to accelerate your systematic review process. +

+ +
+
+
+
+
+
+ ); +} diff --git a/packages/web/src/components/mocks/MockIndex.jsx b/packages/web/src/components/mocks/MockIndex.jsx index d0a24cddc..7c06ab868 100644 --- a/packages/web/src/components/mocks/MockIndex.jsx +++ b/packages/web/src/components/mocks/MockIndex.jsx @@ -18,39 +18,73 @@ export default function MockIndex() {

Available Mocks

- {/* Featured: Complete Workflow Mock */} + {/* Featured Mocks */}

Featured

- -
+ {/* Settings Page Mocks */} + + {/* Other Mocks */}

Other Mocks

diff --git a/packages/web/src/components/mocks/SettingsMockBento.jsx b/packages/web/src/components/mocks/SettingsMockBento.jsx new file mode 100644 index 000000000..4d3e1ea87 --- /dev/null +++ b/packages/web/src/components/mocks/SettingsMockBento.jsx @@ -0,0 +1,489 @@ +/** + * Settings Mock - Bento Box Style + * + * Design Direction: Modern dashboard-inspired bento grid layout. + * Uses varied card sizes, soft shadows, and gradient accents. + * Inspired by Apple's latest design language and modern SaaS dashboards. + */ + +import { For, Show, createSignal } from 'solid-js'; +import { + FiUser, + FiCreditCard, + FiShield, + FiBell, + FiLink, + FiMonitor, + FiMoon, + FiSun, + FiCheck, + FiChevronRight, + FiArrowLeft, + FiMail, + FiCloud, + FiHardDrive, + FiZap, + FiUsers, + FiFolder, + FiLock, + FiSmartphone, + FiGlobe, +} from 'solid-icons/fi'; + +// Mock data +const mockUser = { + name: 'Dr. Sarah Chen', + email: 'sarah.chen@university.edu', + avatar: null, + initials: 'SC', + plan: 'Professional', + planColor: 'from-violet-500 to-purple-600', +}; + +const mockSubscription = { + plan: 'Professional', + status: 'active', + nextBilling: 'Feb 15, 2025', + price: '$29/month', +}; + +const mockUsage = { + projects: { used: 8, limit: 15 }, + collaborators: { used: 12, limit: 25 }, + storage: { used: 2.4, limit: 10, unit: 'GB' }, +}; + +const mockSessions = [ + { device: 'MacBook Pro', location: 'San Francisco, US', current: true, lastActive: 'Now' }, + { device: 'iPhone 15', location: 'San Francisco, US', current: false, lastActive: '2 hours ago' }, + { + device: 'Chrome on Windows', + location: 'New York, US', + current: false, + lastActive: 'Yesterday', + }, +]; + +const mockIntegrations = [ + { name: 'Google Drive', icon: FiCloud, connected: true, color: 'text-blue-500' }, + { name: 'Dropbox', icon: FiHardDrive, connected: false, color: 'text-blue-600' }, + { name: 'Zotero', icon: FiFolder, connected: false, color: 'text-red-500' }, +]; + +// Reusable card component +function BentoCard(props) { + return ( +
+ {props.children} +
+ ); +} + +// Progress ring component +function ProgressRing(props) { + const percentage = () => (props.used / props.limit) * 100; + const circumference = 2 * Math.PI * 36; + const strokeDashoffset = () => circumference - (percentage() / 100) * circumference; + + return ( +
+ + + + +
+ {props.used} + / {props.limit} +
+
+ ); +} + +// Toggle switch component +function Toggle(props) { + return ( + + ); +} + +export default function SettingsMockBento() { + const [darkMode, setDarkMode] = createSignal(false); + const [emailNotifications, setEmailNotifications] = createSignal(true); + const [projectUpdates, setProjectUpdates] = createSignal(true); + const [twoFactorEnabled, setTwoFactorEnabled] = createSignal(false); + + return ( +
+ {/* Header */} +
+
+
+
+ +
+

Settings

+

Manage your account and preferences

+
+
+
+ +
+ {mockUser.initials} +
+
+
+
+
+ + {/* Main Content */} +
+ {/* Bento Grid */} +
+ {/* Profile Card - Large */} + +
+
+
+
+ {mockUser.initials} +
+
+ +
+
+
+

{mockUser.name}

+

{mockUser.email}

+
+ + + {mockUser.plan} + +
+
+
+ +
+
+ + {/* Quick Stats Card */} + +
+

+ Quick Stats +

+
+
+

{mockUsage.projects.used}

+

Projects

+
+
+
+

{mockUsage.collaborators.used}

+

Collaborators

+
+
+
+ + + {/* Subscription Card */} + +
+
+
+
+ +
+ + Active + +
+

{mockSubscription.plan}

+

{mockSubscription.price}

+

+ Next billing: {mockSubscription.nextBilling} +

+ +
+ + + {/* Usage Card */} + +

Usage

+
+
+ +

Projects

+
+
+ +

Collaborators

+
+
+
+ + {/* Storage Card */} + +
+
+ +
+ + {mockUsage.storage.used} / {mockUsage.storage.limit} {mockUsage.storage.unit} + +
+

Storage

+
+
+
+

+ {((mockUsage.storage.used / mockUsage.storage.limit) * 100).toFixed(0)}% used +

+ + + {/* Security Card - Full width */} + +
+
+
+ +
+
+

Security

+

Manage your account security settings

+
+
+
+
+ {/* Password */} +
+
+ +
+

Password

+

Last changed 30 days ago

+ +
+ + {/* Two-Factor */} +
+
+ +
+

Two-Factor Auth

+

+ {twoFactorEnabled() ? 'Enabled' : 'Not enabled'} +

+ +
+ + {/* Sessions */} +
+
+ +
+

Active Sessions

+

{mockSessions.length} devices

+ +
+
+
+ + {/* Notifications Card */} + +
+
+ +
+

Notifications

+
+
+
+
+

Email Notifications

+

Receive important updates via email

+
+ +
+
+
+
+

Project Updates

+

Notifications about project changes

+
+ +
+
+ + + {/* Integrations Card */} + +
+
+
+ +
+

Integrations

+
+ +
+
+ + {integration => { + const Icon = integration.icon; + return ( +
+
+
+ +
+ {integration.name} +
+ + Connect + + } + > + + + Connected + + +
+ ); + }} +
+
+
+ + {/* Active Sessions Card */} + +
+
+
+ +
+
+

Active Sessions

+

Devices currently logged into your account

+
+
+ +
+
+ + {session => ( +
+
+
+ +
+
+
+

{session.device}

+ + + Current + + +
+

+ {session.location} - {session.lastActive} +

+
+
+ + + +
+ )} +
+
+
+
+
+ + {/* Embedded Styles */} + +
+ ); +} diff --git a/packages/web/src/components/mocks/SettingsMockMinimal.jsx b/packages/web/src/components/mocks/SettingsMockMinimal.jsx new file mode 100644 index 000000000..c0f15e175 --- /dev/null +++ b/packages/web/src/components/mocks/SettingsMockMinimal.jsx @@ -0,0 +1,543 @@ +/** + * Settings Mock - Minimal Swiss Style + * + * Design Direction: Clean, typographic, understated elegance. + * Uses strong hierarchy, generous whitespace, and precise alignment. + * Inspired by Swiss design principles and Dieter Rams' aesthetic. + */ + +import { For, Show, createSignal } from 'solid-js'; +import { + FiUser, + FiCreditCard, + FiShield, + FiBell, + FiLink, + FiMonitor, + FiCheck, + FiArrowRight, + FiExternalLink, + FiChevronRight, + FiCloud, + FiKey, + FiSmartphone, + FiMail, + FiLogOut, +} from 'solid-icons/fi'; + +// Mock data +const mockUser = { + name: 'Dr. Sarah Chen', + email: 'sarah.chen@university.edu', + institution: 'Stanford University', + joined: 'December 2024', +}; + +const mockPlan = { + name: 'Professional', + price: 29, + interval: 'month', + features: ['15 Projects', '25 Collaborators', '10 GB Storage', 'Priority Support'], +}; + +const navItems = [ + { id: 'account', label: 'Account', icon: FiUser }, + { id: 'billing', label: 'Billing', icon: FiCreditCard }, + { id: 'security', label: 'Security', icon: FiShield }, + { id: 'notifications', label: 'Notifications', icon: FiBell }, + { id: 'integrations', label: 'Integrations', icon: FiLink }, +]; + +const mockNotificationSettings = [ + { + id: 'email', + title: 'Email Notifications', + description: 'Receive important updates via email', + enabled: true, + }, + { + id: 'projects', + title: 'Project Updates', + description: 'When collaborators make changes', + enabled: true, + }, + { + id: 'mentions', + title: 'Mentions', + description: 'When someone mentions you in a comment', + enabled: false, + }, + { + id: 'marketing', + title: 'Product Updates', + description: 'News about new features and improvements', + enabled: false, + }, +]; + +const mockSessions = [ + { id: 1, device: 'MacBook Pro', browser: 'Safari', location: 'San Francisco', current: true }, + { id: 2, device: 'iPhone 15', browser: 'Safari', location: 'San Francisco', current: false }, + { id: 3, device: 'Windows PC', browser: 'Chrome', location: 'New York', current: false }, +]; + +// Minimal toggle component +function MinimalToggle(props) { + return ( + + ); +} + +// Section component for consistent styling +function Section(props) { + return ( +
+ +

+ {props.title} +

+
+ +

{props.subtitle}

+
+ {props.children} +
+ ); +} + +// Row component for settings items +function SettingRow(props) { + return ( +
+
+

{props.title}

+ +

{props.description}

+
+
+
{props.children}
+
+ ); +} + +export default function SettingsMockMinimal() { + const [activeNav, setActiveNav] = createSignal('account'); + const [notifications, setNotifications] = createSignal( + mockNotificationSettings.reduce((acc, n) => ({ ...acc, [n.id]: n.enabled }), {}), + ); + + const toggleNotification = id => { + setNotifications(prev => ({ ...prev, [id]: !prev[id] })); + }; + + return ( +
+ {/* Top Bar */} +
+
+
+ CoRATES + / + Settings +
+
+ {mockUser.email} +
+ SC +
+
+
+
+ +
+ {/* Sidebar Navigation */} + + + {/* Main Content */} +
+ {/* Account Section */} + +
+

Account

+

Manage your personal information

+
+ +
+
+ {/* Avatar */} +
+
+ SC +
+ +
+ + {/* Info */} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +

+ {mockUser.joined} +

+
+
+
+
+
+ +
+
+
+
+

Delete Account

+

+ Permanently delete your account and all associated data +

+
+ +
+
+
+
+ + {/* Billing Section */} + +
+

Billing

+

Manage your subscription and payments

+
+ +
+ {/* Current Plan */} +
+
+
+
+

+ Current Plan +

+

{mockPlan.name}

+
+ + Active + +
+ +
+ ${mockPlan.price} + /{mockPlan.interval} +
+ +
    + + {feature => ( +
  • + + {feature} +
  • + )} +
    +
+
+ + {/* Quick Actions */} +
+ + + +
+
+
+ +
+
+ {[ + { label: 'Projects', used: 8, limit: 15 }, + { label: 'Collaborators', used: 12, limit: 25 }, + { label: 'Storage', used: 2.4, limit: 10, suffix: 'GB' }, + ].map(stat => ( +
+

+ {stat.label} +

+

+ {stat.used} + + /{stat.limit} + {stat.suffix || ''} + +

+
+
+
+
+ ))} +
+
+
+ + {/* Security Section */} + +
+

Security

+

Protect your account

+
+ +
+ + + + + + + + + + + +
+ +
+
+ + {session => ( +
+
+
+ +
+
+

+ {session.device} + + + Current + + +

+

+ {session.browser} - {session.location} +

+
+
+ + + +
+ )} +
+
+
+
+ + {/* Notifications Section */} + +
+

Notifications

+

Control how you receive updates

+
+ +
+ + {setting => ( + + toggleNotification(setting.id)} + /> + + )} + +
+
+ + {/* Integrations Section */} + +
+

Integrations

+

Connect third-party services

+
+ +
+ {[ + { + name: 'Google Drive', + description: 'Import PDFs directly from your Drive', + icon: FiCloud, + connected: true, + }, + { + name: 'Zotero', + description: 'Sync your reference library', + icon: FiLink, + connected: false, + }, + { + name: 'Mendeley', + description: 'Import references and annotations', + icon: FiLink, + connected: false, + }, + ].map(integration => { + const Icon = integration.icon; + return ( +
+
+
+ +
+
+

{integration.name}

+

{integration.description}

+
+
+ + Connect + + } + > +
+ + + Connected + + +
+
+
+ ); + })} +
+
+ + {/* Save Button */} +
+ +
+
+
+ + {/* Embedded Styles */} + +
+ ); +} From bcb8275dd7974d87624d38ff40b8bfb8310468c1 Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Fri, 9 Jan 2026 18:27:33 -0600 Subject: [PATCH 02/15] imrpove auth loading state --- packages/web/src/api/better-auth-store.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/web/src/api/better-auth-store.js b/packages/web/src/api/better-auth-store.js index 4cfe1b580..266fa3197 100644 --- a/packages/web/src/api/better-auth-store.js +++ b/packages/web/src/api/better-auth-store.js @@ -175,6 +175,22 @@ function createBetterAuthStore() { }); }); + // Enhanced authLoading that accounts for cached data + // If we have cached user data, we're not "loading" from UI perspective + const isAuthLoading = () => { + // If offline, we're not loading - we use cached data + if (!isOnline()) { + return false; + } + // If session is pending but we have cached data, don't show loading state + // This prevents UI flash when we have data to show + if (authLoading() && cachedUser()) { + return false; + } + // Otherwise return the actual session pending state + return authLoading(); + }; + // Combined signals that use cached data when offline const isLoggedIn = () => { if (isOnline()) { @@ -852,7 +868,7 @@ function createBetterAuthStore() { isLoggedIn, isAuthenticated, user, - authLoading, + authLoading: isAuthLoading, authError, isOnline, From ce37b01ef1fc1db99cc66db879a74c98d8955cab Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Fri, 9 Jan 2026 21:56:03 -0600 Subject: [PATCH 03/15] initial migration of the main dashboard --- .../docs/plans/study-counts-architecture.md | 261 +++++++++++++++++ packages/web/src/Routes.jsx | 2 +- .../src/components/dashboard/ActivityFeed.jsx | 135 +++++++++ .../src/components/dashboard/Dashboard.jsx | 264 +++++++++++++++++ .../components/dashboard/DashboardHeader.jsx | 64 +++++ .../dashboard/DashboardSkeleton.jsx | 145 ++++++++++ .../dashboard/LocalAppraisalCard.jsx | 121 ++++++++ .../dashboard/LocalAppraisalsSection.jsx | 201 +++++++++++++ .../src/components/dashboard/ProgressCard.jsx | 81 ++++++ .../src/components/dashboard/ProjectCard.jsx | 213 ++++++++++++++ .../components/dashboard/ProjectsSection.jsx | 270 ++++++++++++++++++ .../src/components/dashboard/QuickActions.jsx | 110 +++++++ .../web/src/components/dashboard/StatsRow.jsx | 105 +++++++ .../web/src/components/dashboard/index.js | 18 ++ packages/web/src/global.css | 23 ++ 15 files changed, 2012 insertions(+), 1 deletion(-) create mode 100644 packages/docs/plans/study-counts-architecture.md create mode 100644 packages/web/src/components/dashboard/ActivityFeed.jsx create mode 100644 packages/web/src/components/dashboard/Dashboard.jsx create mode 100644 packages/web/src/components/dashboard/DashboardHeader.jsx create mode 100644 packages/web/src/components/dashboard/DashboardSkeleton.jsx create mode 100644 packages/web/src/components/dashboard/LocalAppraisalCard.jsx create mode 100644 packages/web/src/components/dashboard/LocalAppraisalsSection.jsx create mode 100644 packages/web/src/components/dashboard/ProgressCard.jsx create mode 100644 packages/web/src/components/dashboard/ProjectCard.jsx create mode 100644 packages/web/src/components/dashboard/ProjectsSection.jsx create mode 100644 packages/web/src/components/dashboard/QuickActions.jsx create mode 100644 packages/web/src/components/dashboard/StatsRow.jsx create mode 100644 packages/web/src/components/dashboard/index.js diff --git a/packages/docs/plans/study-counts-architecture.md b/packages/docs/plans/study-counts-architecture.md new file mode 100644 index 000000000..d4d77b40e --- /dev/null +++ b/packages/docs/plans/study-counts-architecture.md @@ -0,0 +1,261 @@ +# Study Counts Architecture + +## Problem + +The dashboard shows "0/0 studies" for all projects because: + +1. Studies are stored in Yjs durable objects (`ProjectDoc`), not SQL +2. The `/api/users/me/projects` endpoint only returns SQL data (no study counts) +3. Getting accurate counts requires connecting to each ProjectDoc + +## Solution Overview + +Two-tier approach: + +1. **Lazy Load (Authoritative)**: Fetch true counts when user opens a project via the existing Yjs WebSocket connection +2. **Notifications (Preview)**: Push approximate counts to dashboard for quick previews (can be stale, max 50 queued) + +--- + +## Phase 1: Lazy Load (Authoritative Source) + +When a user opens a project, they connect to `ProjectDoc` via WebSocket. At that point, we have access to the Yjs document and can compute accurate study counts. + +### 1.1 Add Stats Computation to ProjectDoc + +**File**: `packages/workers/src/durable-objects/ProjectDoc.js` + +Add a method to compute stats from the Yjs document: + +```javascript +getProjectStats() { + const reviews = this.doc.getMap('reviews'); + const studyCount = reviews.size; + + let completedCount = 0; + reviews.forEach((study) => { + // Check if study has completed status + // Structure depends on checklist type - need to verify + if (study.status === 'completed' || study.completed) { + completedCount++; + } + }); + + return { studyCount, completedCount }; +} +``` + +### 1.2 Send Stats on WebSocket Connect + +When a client connects to ProjectDoc, send stats as an initial message: + +```javascript +// In WebSocket connection handler +ws.send( + JSON.stringify({ + type: 'project-stats', + stats: this.getProjectStats(), + }), +); +``` + +### 1.3 Frontend: Store Stats in Project Store + +**File**: `packages/web/src/stores/projectStore.js` + +Add stats storage: + +```javascript +// Add to store +projectStats: {}, // { [projectId]: { studyCount, completedCount, lastUpdated } } + +setProjectStats(projectId, stats) { + setStore('projectStats', projectId, { + ...stats, + lastUpdated: Date.now() + }); +} +``` + +### 1.4 Frontend: Handle Stats Message in useProject + +**File**: `packages/web/src/primitives/useProject.js` + +Listen for `project-stats` message type: + +```javascript +// In WebSocket message handler +if (data.type === 'project-stats') { + projectStore.setProjectStats(projectId, data.stats); +} +``` + +### 1.5 Frontend: Update Stats on Study Changes + +When studies are added/removed locally, update the cached stats: + +```javascript +// After adding a study +const currentStats = projectStore.projectStats[projectId]; +projectStore.setProjectStats(projectId, { + studyCount: currentStats.studyCount + 1, + completedCount: currentStats.completedCount, +}); +``` + +### 1.6 Dashboard: Display Cached Stats + +**File**: `packages/web/src/components/dashboard/ProjectCard.jsx` + +Read from projectStore for cards of previously-opened projects: + +```javascript +const cachedStats = () => projectStore.projectStats[props.project.id]; +const studyCount = () => cachedStats()?.studyCount ?? props.project.studyCount ?? 0; +const completedCount = () => cachedStats()?.completedCount ?? props.project.completedCount ?? 0; +``` + +Priority: `projectStore cache > props from API > 0` + +--- + +## Phase 2: Notifications (Preview/Hints) + +Notifications provide approximate stats for the dashboard before a user opens a project. These are secondary and may be incomplete (max 50 pending notifications, can be lost). + +### 2.1 Emit Stats Updates from ProjectDoc + +**File**: `packages/workers/src/durable-objects/ProjectDoc.js` + +When studies change, notify project members via their UserSession: + +```javascript +async broadcastStatsToMembers(env) { + const stats = this.getProjectStats(); + const members = await this.getProjectMembers(env); // Need to implement + + for (const member of members) { + const session = env.USER_SESSION.idFromName(member.userId); + await session.get().fetch('https://internal/notify', { + method: 'POST', + body: JSON.stringify({ + type: 'project-stats-updated', + projectId: this.projectId, + stats, + timestamp: Date.now() + }) + }); + } +} +``` + +### 2.2 Call Stats Broadcast on Study Changes + +Hook into Yjs observers to detect changes: + +```javascript +this.doc.getMap('reviews').observe((event) => { + // Debounce to avoid spamming on bulk imports + this.scheduleStatsBroadcast(); +}); + +scheduleStatsBroadcast() { + if (this.statsBroadcastTimer) return; + this.statsBroadcastTimer = setTimeout(() => { + this.broadcastStatsToMembers(this.env); + this.statsBroadcastTimer = null; + }, 5000); // 5 second debounce +} +``` + +### 2.3 Frontend: Handle Stats Notifications + +**File**: `packages/web/src/primitives/useMembershipSync.js` + +Add handler for stats updates: + +```javascript +if (notificationType === 'project-stats-updated') { + // Store as hint in projectStore (lower priority than lazy-loaded) + projectStore.setProjectStatsHint(notification.projectId, notification.stats); +} +``` + +### 2.4 API Enhancement (Optional) + +Could add stats hints to `/api/users/me/projects` response by: + +1. Storing last-known stats in SQL when ProjectDoc broadcasts +2. Joining that data in the projects query + +This avoids N+1 queries to ProjectDoc DOs at dashboard load time. + +--- + +## Data Flow Summary + +``` +Dashboard Load: + API returns projects (no stats or stale stats from SQL cache) + -> Show cached stats from projectStore if available + -> Show notification hints if available + -> Show "-- studies" if no data + +User Opens Project: + WebSocket connects to ProjectDoc + -> ProjectDoc sends project-stats message + -> Frontend stores authoritative stats in projectStore + -> Dashboard updates to show true counts + +Study Added/Removed (while in project): + Frontend updates local stats immediately + ProjectDoc broadcasts to other members (5s debounce) + -> Other members see updated hints on dashboard +``` + +--- + +## Implementation Order + +### Phase 1 Tasks (Lazy Load) + +1. [ ] Add `getProjectStats()` method to ProjectDoc +2. [ ] Send stats on WebSocket connect in ProjectDoc +3. [ ] Add `projectStats` to projectStore +4. [ ] Handle `project-stats` message in useProject +5. [ ] Update Dashboard/ProjectCard to read from projectStore +6. [ ] Update local stats on study add/remove + +### Phase 2 Tasks (Notifications) + +1. [ ] Add `broadcastStatsToMembers()` to ProjectDoc +2. [ ] Add Yjs observer with debounce for stats changes +3. [ ] Add `project-stats-updated` handler to useMembershipSync +4. [ ] Add `projectStatsHints` to projectStore (separate from authoritative) +5. [ ] Optional: Cache stats in SQL for API response + +--- + +## Files to Modify + +**Phase 1:** + +- `packages/workers/src/durable-objects/ProjectDoc.js` - Stats computation + WS message +- `packages/web/src/stores/projectStore.js` - Stats storage +- `packages/web/src/primitives/useProject.js` - Handle stats message +- `packages/web/src/components/dashboard/ProjectCard.jsx` - Display cached stats +- `packages/web/src/components/dashboard/Dashboard.jsx` - Stats aggregation + +**Phase 2:** + +- `packages/workers/src/durable-objects/ProjectDoc.js` - Stats broadcast +- `packages/web/src/primitives/useMembershipSync.js` - Handle notification +- `packages/web/src/stores/projectStore.js` - Stats hints storage + +--- + +## Open Questions + +1. **Study completion criteria**: What field indicates a study is "completed"? Need to check Yjs document structure. +2. **Member list access**: How does ProjectDoc get the list of project members? May need to query SQL or store in Yjs. +3. **Performance**: For projects with many members, broadcasting could be expensive. May want to limit or batch. diff --git a/packages/web/src/Routes.jsx b/packages/web/src/Routes.jsx index dedf6e0df..a3c8457d6 100644 --- a/packages/web/src/Routes.jsx +++ b/packages/web/src/Routes.jsx @@ -1,6 +1,6 @@ import { Router, Route } from '@solidjs/router'; import { lazy } from 'solid-js'; -import Dashboard from './components/Dashboard.jsx'; +import Dashboard from './components/dashboard/index.js'; import SignIn from '@/components/auth/SignIn.jsx'; import SignUp from '@/components/auth/SignUp.jsx'; import CheckEmail from '@/components/auth/CheckEmail.jsx'; diff --git a/packages/web/src/components/dashboard/ActivityFeed.jsx b/packages/web/src/components/dashboard/ActivityFeed.jsx new file mode 100644 index 000000000..787fbc47a --- /dev/null +++ b/packages/web/src/components/dashboard/ActivityFeed.jsx @@ -0,0 +1,135 @@ +/** + * ActivityFeed - Shows recent activity across projects + */ + +import { Show, For, createMemo } from 'solid-js'; +import { FiClock, FiFileText, FiFolder, FiCheck, FiUser } from 'solid-icons/fi'; + +/** + * Format a relative time string + * @param {Date|string} date + * @returns {string} + */ +function formatRelativeTime(date) { + const now = new Date(); + const then = new Date(date); + const diffMs = now - then; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + return then.toLocaleDateString(); +} + +/** + * Icon for activity type + */ +function ActivityIcon(props) { + const iconMap = { + study: , + project: , + complete: , + user: , + default: , + }; + + const bgMap = { + study: 'bg-blue-100 text-blue-600', + project: 'bg-amber-100 text-amber-600', + complete: 'bg-emerald-100 text-emerald-600', + user: 'bg-violet-100 text-violet-600', + default: 'bg-stone-100 text-stone-600', + }; + + return ( +
+ {iconMap[props.type] || iconMap.default} +
+ ); +} + +/** + * Single activity item + */ +function ActivityItem(props) { + return ( +
+ +
+

+ {props.activity.title} + + {props.activity.subtitle} + +

+

{formatRelativeTime(props.activity.timestamp)}

+
+
+ ); +} + +/** + * Empty state for no activity + */ +function EmptyActivity() { + return ( +
+
+ +
+

No recent activity

+

Your activity will appear here as you work

+
+ ); +} + +/** + * Activity feed component + * @param {Object} props + * @param {Array} props.activities - Array of activity objects + * @param {number} [props.limit] - Maximum number of activities to show + * @param {() => void} [props.onViewAll] - Handler for view all link + */ +export function ActivityFeed(props) { + const displayActivities = createMemo(() => { + const activities = props.activities || []; + const limit = props.limit || 5; + return activities.slice(0, limit); + }); + + return ( +
+
+

+ Recent Activity +

+ (props.limit || 5)}> + + +
+ + 0} fallback={}> +
+ {activity => } +
+
+
+ ); +} + +export default ActivityFeed; diff --git a/packages/web/src/components/dashboard/Dashboard.jsx b/packages/web/src/components/dashboard/Dashboard.jsx new file mode 100644 index 000000000..73df7d54f --- /dev/null +++ b/packages/web/src/components/dashboard/Dashboard.jsx @@ -0,0 +1,264 @@ +/** + * Dashboard - Main dashboard container with state machine + * + * Handles multiple user states: logged-out, loading, no-plan, active, etc. + */ + +import { createMemo, Show, Switch, Match } from 'solid-js'; +import { useNavigate } from '@solidjs/router'; +import { useBetterAuth } from '@api/better-auth-store.js'; +import { useSubscription } from '@primitives/useSubscription.js'; +import { useMyProjectsList } from '@primitives/useMyProjectsList.js'; +import localChecklistsStore from '@/stores/localChecklistsStore'; + +import DashboardHeader from './DashboardHeader.jsx'; +import { StatsRow } from './StatsRow.jsx'; +import ProgressCard from './ProgressCard.jsx'; +import QuickActions from './QuickActions.jsx'; +import ActivityFeed from './ActivityFeed.jsx'; +import DashboardSkeleton from './DashboardSkeleton.jsx'; +import { ProjectsSection } from './ProjectsSection.jsx'; +import { LocalAppraisalsSection } from './LocalAppraisalsSection.jsx'; + +/** + * Dashboard state machine + * Determines which UI state to show based on auth/subscription status + */ +function useDashboardState() { + const { isLoggedIn, authLoading, user, isOnline } = useBetterAuth(); + const { + loading: subscriptionLoading, + subscriptionFetchFailed, + hasEntitlement, + hasQuota, + } = useSubscription(); + const { projects, isInitialLoading: projectsLoading } = useMyProjectsList(); + const { checklists } = localChecklistsStore; + + const state = createMemo(() => { + if (authLoading()) return 'loading'; + if (!isLoggedIn()) return 'logged-out'; + if (subscriptionLoading() && !projects().length) return 'loading-subscription'; + if (subscriptionFetchFailed()) return 'subscription-error'; + return 'active'; + }); + + const canCreateProject = createMemo(() => { + if (!isOnline()) return false; + if (!isLoggedIn()) return false; + if (!hasEntitlement('project.create')) return false; + const projectCount = projects().length; + return hasQuota('projects.max', { used: projectCount, requested: 1 }); + }); + + // Compute stats from real data + const stats = createMemo(() => { + const projectList = projects() || []; + const localList = checklists() || []; + + // Calculate completed studies across all projects + let totalStudies = 0; + let completedStudies = 0; + projectList.forEach(project => { + const studies = project.studyCount || 0; + const completed = project.completedCount || 0; + totalStudies += studies; + completedStudies += completed; + }); + + // Count unique team members across projects + const memberIds = new Set(); + projectList.forEach(project => { + if (project.members) { + project.members.forEach(m => memberIds.add(m.userId || m.id)); + } + }); + + return { + projectCount: projectList.length, + completedStudies, + totalStudies, + localAppraisalCount: localList.length, + teamMemberCount: memberIds.size || undefined, + }; + }); + + // Generate recent activity from projects + const activities = createMemo(() => { + const projectList = projects() || []; + const activities = []; + + projectList.slice(0, 5).forEach(project => { + activities.push({ + type: 'project', + title: project.name, + subtitle: 'was updated', + timestamp: project.updatedAt || project.createdAt, + }); + }); + + return activities.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)).slice(0, 5); + }); + + return { + state, + user, + isOnline, + isLoggedIn, + canCreateProject, + stats, + activities, + projectsLoading, + subscriptionFetchFailed, + }; +} + +export function Dashboard() { + const navigate = useNavigate(); + const { state, user, isOnline, canCreateProject, stats, activities, subscriptionFetchFailed } = + useDashboardState(); + + const handleCreateProject = () => { + // Trigger the project creation modal through the ProjectsPanel + // For now, just scroll to projects section + document.getElementById('projects-section')?.scrollIntoView({ behavior: 'smooth' }); + }; + + const handleStartROBINSI = () => { + navigate('/checklist/new?type=robins-i'); + }; + + const handleStartAMSTAR2 = () => { + navigate('/checklist/new?type=amstar-2'); + }; + + const handleLearnMore = () => { + window.open('https://docs.corates.app', '_blank'); + }; + + return ( + + {/* Loading state */} + + + + + {/* Logged out state */} + +
+ + + {/* Main content grid */} +
+ {/* Left column - Local appraisals */} +
+ +
+ + {/* Right sidebar */} +
+ +
+
+
+
+ + {/* Active state (logged in) */} + +
+ {/* Subscription error banner */} + + + + + + + + + {/* Main content grid */} +
+ {/* Left column - Projects and Local appraisals */} +
+ + +
+ + {/* Right sidebar */} +
+ + + + + +
+
+
+
+ + {/* Subscription error fallback */} + +
+ + +
+

+ Unable to load your subscription. Please try refreshing the page. +

+
+ + {/* Still show content */} +
+
+ + +
+
+ +
+
+
+
+
+ ); +} + +export default Dashboard; diff --git a/packages/web/src/components/dashboard/DashboardHeader.jsx b/packages/web/src/components/dashboard/DashboardHeader.jsx new file mode 100644 index 000000000..59c928ce4 --- /dev/null +++ b/packages/web/src/components/dashboard/DashboardHeader.jsx @@ -0,0 +1,64 @@ +/** + * DashboardHeader - Welcome section with user info and actions + */ + +import { Show } from 'solid-js'; +import { FiPlus, FiSearch } from 'solid-icons/fi'; + +/** + * @param {Object} props + * @param {Object} props.user - User object { name, email, image } + * @param {boolean} props.canCreateProject - Whether user can create projects + * @param {boolean} props.isOnline - Whether user is online + * @param {Function} props.onCreateProject - Called when create project button clicked + * @param {Function} props.onSearch - Called when search button clicked (optional) + */ +export function DashboardHeader(props) { + const firstName = () => { + const name = props.user?.name || ''; + return name.split(' ')[0] || 'there'; + }; + + return ( +
+
+
+

Welcome back,

+

+ {firstName()} +

+ +

{props.user.email}

+
+
+
+ + + + + + +
+
+
+ ); +} + +export default DashboardHeader; diff --git a/packages/web/src/components/dashboard/DashboardSkeleton.jsx b/packages/web/src/components/dashboard/DashboardSkeleton.jsx new file mode 100644 index 000000000..87c93f456 --- /dev/null +++ b/packages/web/src/components/dashboard/DashboardSkeleton.jsx @@ -0,0 +1,145 @@ +/** + * DashboardSkeleton - Loading skeleton for dashboard + */ + +/** + * Pulsing skeleton bar + */ +function SkeletonBar(props) { + return ( +
+ ); +} + +/** + * Skeleton for stat cards + */ +function StatCardSkeleton() { + return ( +
+
+
+ + +
+ +
+ +
+ ); +} + +/** + * Skeleton for progress card + */ +function ProgressCardSkeleton() { + return ( +
+ + +
+ + +
+
+ ); +} + +/** + * Skeleton for quick action card + */ +function QuickActionSkeleton() { + return ( +
+ +
+ + +
+
+ ); +} + +/** + * Skeleton for activity item + */ +function ActivityItemSkeleton() { + return ( +
+ +
+ + +
+
+ ); +} + +/** + * Full dashboard skeleton + */ +export function DashboardSkeleton() { + return ( +
+ {/* Header skeleton */} +
+
+ + +
+ +
+ + {/* Stats row skeleton */} +
+ + + + +
+ + {/* Main content grid skeleton */} +
+ {/* Left column */} +
+ {/* Projects section */} +
+ +
+ + + +
+
+
+ + {/* Right sidebar */} +
+ + + {/* Quick actions skeleton */} +
+ +
+ + + +
+
+ + {/* Activity feed skeleton */} +
+ +
+ + + +
+
+
+
+
+ ); +} + +export default DashboardSkeleton; diff --git a/packages/web/src/components/dashboard/LocalAppraisalCard.jsx b/packages/web/src/components/dashboard/LocalAppraisalCard.jsx new file mode 100644 index 000000000..75499bab3 --- /dev/null +++ b/packages/web/src/components/dashboard/LocalAppraisalCard.jsx @@ -0,0 +1,121 @@ +/** + * LocalAppraisalCard - Compact card for local appraisals + * + * Features: + * - Horizontal layout + * - Checklist type badge + * - Relative timestamp + * - Inline rename with Editable + * - Delete action + */ + +import { Show, createMemo } from 'solid-js'; +import { FiFileText, FiArrowRight, FiTrash2 } from 'solid-icons/fi'; +import { Editable } from '@corates/ui'; +import { getChecklistMetadata } from '@/checklist-registry'; + +/** + * Format a relative time string + * @param {Date|string|number} date + * @returns {string} + */ +function formatRelativeTime(date) { + if (!date) return ''; + const now = new Date(); + const then = new Date(date); + const diffMs = now - then; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`; + return then.toLocaleDateString(); +} + +/** + * Local appraisal card component + * @param {Object} props + * @param {Object} props.checklist - Checklist data + * @param {() => void} props.onOpen - Called when card is opened + * @param {() => void} [props.onDelete] - Called when delete is requested + * @param {(name: string) => void} [props.onRename] - Called when renamed + * @param {number} [props.delay] - Animation delay in ms + */ +export function LocalAppraisalCard(props) { + const typeLabel = createMemo(() => { + const metadata = getChecklistMetadata(props.checklist?.type || props.checklist?.checklistType); + return metadata?.name || props.checklist?.type || 'Checklist'; + }); + + const relativeTime = createMemo(() => { + return formatRelativeTime(props.checklist?.updatedAt || props.checklist?.createdAt); + }); + + return ( +
+ {/* Icon */} +
+ +
+ + {/* Content */} +
+ {props.checklist?.name} + } + > + props.onRename?.(newName)} + /> + +
+ + {typeLabel()} + + {relativeTime()} +
+
+ + {/* Actions */} +
+ + + + +
+
+ ); +} + +export default LocalAppraisalCard; diff --git a/packages/web/src/components/dashboard/LocalAppraisalsSection.jsx b/packages/web/src/components/dashboard/LocalAppraisalsSection.jsx new file mode 100644 index 000000000..fede5c2a2 --- /dev/null +++ b/packages/web/src/components/dashboard/LocalAppraisalsSection.jsx @@ -0,0 +1,201 @@ +/** + * LocalAppraisalsSection - Section for device-local appraisals + * + * Features: + * - Compact horizontal card layout + * - Create new appraisal button + * - Delete with confirmation + * - Inline rename + * - Sign-in prompt for logged-out users + */ + +import { Show, For } from 'solid-js'; +import { useNavigate } from '@solidjs/router'; +import { useConfirmDialog } from '@corates/ui'; +import { FiPlus, FiFileText, FiLogIn } from 'solid-icons/fi'; + +import localChecklistsStore from '@/stores/localChecklistsStore'; +import { LocalAppraisalCard } from './LocalAppraisalCard.jsx'; + +/** + * Empty state for no local appraisals + */ +function EmptyLocalState(props) { + return ( +
+
+ +
+

No local appraisals

+

+ Create appraisals that stay on this device +

+ +
+ ); +} + +/** + * Sign-in prompt banner + */ +function SignInPrompt(props) { + return ( +
+
+
+ +
+
+

Want to collaborate?

+

Sign in to create projects and sync across devices

+
+
+ +
+ ); +} + +/** + * Loading skeleton for local appraisals + */ +function LocalAppraisalSkeleton(props) { + return ( +
+
+
+
+
+
+
+
+
+
+
+ ); +} + +/** + * Local appraisals section component + * @param {Object} props + * @param {boolean} [props.showHeader] - Whether to show section header + * @param {boolean} [props.showSignInPrompt] - Whether to show sign-in prompt + */ +export function LocalAppraisalsSection(props) { + const navigate = useNavigate(); + const confirmDialog = useConfirmDialog(); + + const { checklists, loading, deleteChecklist, updateChecklist } = localChecklistsStore; + + const handleOpen = checklistId => { + navigate(`/checklist/${checklistId}`); + }; + + const handleDelete = async checklistId => { + const confirmed = await confirmDialog.open({ + title: 'Delete Appraisal', + description: 'Are you sure you want to delete this appraisal? This cannot be undone.', + confirmText: 'Delete', + variant: 'danger', + }); + if (confirmed) { + await deleteChecklist(checklistId); + } + }; + + const handleRename = async (checklistId, newName) => { + await updateChecklist(checklistId, { name: newName }); + }; + + const handleCreate = () => { + navigate('/checklist'); + }; + + const handleSignIn = () => { + navigate('/signin'); + }; + + const hasChecklists = () => checklists()?.length > 0; + + return ( +
+ {/* Header */} + +
+

+ Local Appraisals +

+ + + +
+
+ + {/* Sign-in prompt */} + +
+ +
+
+ + {/* Appraisals list */} +
+ {/* Loading skeletons */} + + + + + + {/* Empty state */} + + + + + {/* Appraisal cards */} + + {(checklist, index) => ( + handleRename(checklist.id, newName)} + delay={index() * 50} + /> + )} + +
+ + +
+ ); +} + +export default LocalAppraisalsSection; diff --git a/packages/web/src/components/dashboard/ProgressCard.jsx b/packages/web/src/components/dashboard/ProgressCard.jsx new file mode 100644 index 000000000..b59919a3d --- /dev/null +++ b/packages/web/src/components/dashboard/ProgressCard.jsx @@ -0,0 +1,81 @@ +/** + * ProgressCard - Shows overall progress with an SVG arc visualization + */ + +import { createMemo, Show } from 'solid-js'; + +/** + * SVG arc progress indicator + * @param {Object} props + * @param {number} props.completed - Number of completed items + * @param {number} props.total - Total number of items + * @param {string} [props.title] - Card title + * @param {string} [props.subtitle] - Subtitle text + */ +export function ProgressCard(props) { + const percentage = createMemo(() => { + if (!props.total || props.total === 0) return 0; + return Math.round((props.completed / props.total) * 100); + }); + + // SVG arc calculations + const radius = 52; + const circumference = 2 * Math.PI * radius; + const strokeDashoffset = createMemo(() => { + return circumference - (percentage() / 100) * circumference; + }); + + return ( +
+

{props.title || 'Overall Progress'}

+ + {/* SVG Arc */} +
+ + {/* Background arc */} + + {/* Progress arc */} + + + {/* Center text */} +
+ {percentage()}% + complete +
+
+ + {/* Stats below */} +
+
+ + {props.completed} done +
+
+ + {props.total - props.completed} remaining +
+
+ + +

{props.subtitle}

+
+
+ ); +} + +export default ProgressCard; diff --git a/packages/web/src/components/dashboard/ProjectCard.jsx b/packages/web/src/components/dashboard/ProjectCard.jsx new file mode 100644 index 000000000..43369e158 --- /dev/null +++ b/packages/web/src/components/dashboard/ProjectCard.jsx @@ -0,0 +1,213 @@ +/** + * ProjectCard - Enhanced project card with accent colors and progress bar + * + * Features: + * - Deterministic accent color based on project ID + * - Progress bar with gradient + * - Role badge (Lead/Reviewer) + * - Member count + * - Relative timestamp + * - Hover effects and animations + */ + +import { Show, createMemo } from 'solid-js'; +import { FiUsers, FiChevronRight, FiTrash2 } from 'solid-icons/fi'; + +/** + * Accent color configurations + */ +const ACCENT_COLORS = [ + { + name: 'blue', + bg: 'bg-blue-50', + text: 'text-blue-700', + fill: 'bg-blue-500', + gradient: 'from-blue-500 to-blue-600', + }, + { + name: 'amber', + bg: 'bg-amber-50', + text: 'text-amber-700', + fill: 'bg-amber-500', + gradient: 'from-amber-400 to-orange-500', + }, + { + name: 'rose', + bg: 'bg-rose-50', + text: 'text-rose-700', + fill: 'bg-rose-500', + gradient: 'from-rose-400 to-pink-500', + }, + { + name: 'emerald', + bg: 'bg-emerald-50', + text: 'text-emerald-700', + fill: 'bg-emerald-500', + gradient: 'from-emerald-400 to-teal-500', + }, + { + name: 'violet', + bg: 'bg-violet-50', + text: 'text-violet-700', + fill: 'bg-violet-500', + gradient: 'from-violet-400 to-purple-500', + }, +]; + +/** + * Generate a deterministic color index from a string (project ID) + * @param {string} id - Project ID + * @returns {number} - Color index + */ +function hashToColorIndex(id) { + if (!id) return 0; + let hash = 0; + for (let i = 0; i < id.length; i++) { + hash = (hash << 5) - hash + id.charCodeAt(i); + hash |= 0; + } + return Math.abs(hash) % ACCENT_COLORS.length; +} + +/** + * Format a relative time string + * @param {Date|string|number} date + * @returns {string} + */ +function formatRelativeTime(date) { + if (!date) return ''; + const now = new Date(); + const then = new Date(date); + const diffMs = now - then; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + if (diffDays < 7) return `${diffDays}d ago`; + if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`; + return then.toLocaleDateString(); +} + +/** + * Enhanced project card component + * @param {Object} props + * @param {Object} props.project - Project data + * @param {() => void} props.onOpen - Called when project is opened + * @param {() => void} [props.onDelete] - Called when delete is requested + * @param {number} [props.delay] - Animation delay in ms + */ +export function ProjectCard(props) { + const colors = createMemo(() => { + const index = hashToColorIndex(props.project?.id); + return ACCENT_COLORS[index]; + }); + + const progress = createMemo(() => { + const completed = props.project?.completedCount || 0; + const total = props.project?.studyCount || 0; + if (total === 0) return { completed: 0, total: 0, percentage: 0 }; + return { + completed, + total, + percentage: Math.round((completed / total) * 100), + }; + }); + + const relativeTime = createMemo(() => { + return formatRelativeTime(props.project?.updatedAt || props.project?.createdAt); + }); + + const memberCount = createMemo(() => { + return props.project?.memberCount || props.project?.members?.length || 1; + }); + + const isOwner = () => props.project?.role === 'owner'; + + return ( +
+ {/* Decorative corner accent */} +
+ + {/* Header */} +
+
+
+ + {isOwner() ? 'Lead' : 'Reviewer'} + + {relativeTime()} +
+

+ {props.project?.name} +

+
+ + {/* Delete button for owners */} + + + +
+ + {/* Description */} +

+ {props.project?.description || 'No description'} +

+ + {/* Progress bar */} +
+
+ Progress + + {progress().completed}/{progress().total} studies + +
+
+
+
+
+ + {/* Footer */} +
+
+ + + {memberCount()} member{memberCount() !== 1 ? 's' : ''} + +
+ +
+
+ ); +} + +export default ProjectCard; diff --git a/packages/web/src/components/dashboard/ProjectsSection.jsx b/packages/web/src/components/dashboard/ProjectsSection.jsx new file mode 100644 index 000000000..ba6d31256 --- /dev/null +++ b/packages/web/src/components/dashboard/ProjectsSection.jsx @@ -0,0 +1,270 @@ +/** + * ProjectsSection - Projects grid for dashboard + * + * Displays projects with the enhanced ProjectCard design, + * handles create form, empty states, and loading states. + */ + +import { createSignal, Show, For } from 'solid-js'; +import { useNavigate } from '@solidjs/router'; +import { useQueryClient } from '@tanstack/solid-query'; +import { useConfirmDialog, showToast } from '@corates/ui'; +import { FiPlus, FiFolder } from 'solid-icons/fi'; + +import { useMyProjectsList } from '@primitives/useMyProjectsList.js'; +import { useBetterAuth } from '@api/better-auth-store.js'; +import { useSubscription } from '@primitives/useSubscription.js'; +import { API_BASE } from '@config/api.js'; +import { queryKeys } from '@lib/queryKeys.js'; +import projectStore from '@/stores/projectStore.js'; + +import { ProjectCard } from './ProjectCard.jsx'; +import CreateProjectForm from '@/components/project/CreateProjectForm.jsx'; +import ContactPrompt from '@/components/project/ContactPrompt.jsx'; + +/** + * Skeleton card for loading state + */ +function ProjectCardSkeleton(props) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} + +/** + * Empty state when no projects exist + */ +function EmptyProjectsState(props) { + return ( +
+
+ +
+

No projects yet

+

+ Create your first project to start collaborating on evidence synthesis with your team. +

+ + + +
+ ); +} + +/** + * Projects section component + * @param {Object} props + * @param {boolean} [props.showHeader] - Whether to show section header + * @param {() => void} [props.onCreateClick] - External handler for create button + */ +export function ProjectsSection(props) { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const confirmDialog = useConfirmDialog(); + const { isOnline } = useBetterAuth(); + + // Projects data + const projectListQuery = useMyProjectsList(); + const projects = () => projectListQuery.projects(); + const projectCount = () => projects()?.length || 0; + + // Subscription checks + const { hasEntitlement, hasQuota, loading: subscriptionLoading } = useSubscription(); + + const [showCreateForm, setShowCreateForm] = createSignal(false); + + const canCreateProject = () => { + if (subscriptionLoading()) return null; + return ( + hasEntitlement('project.create') && + hasQuota('projects.max', { used: projectCount(), requested: 1 }) + ); + }; + + const restrictionType = () => { + if (subscriptionLoading()) return null; + return !hasEntitlement('project.create') ? 'entitlement' : 'quota'; + }; + + // Handlers + const handleProjectCreated = ( + newProject, + pendingPdfs = [], + pendingRefs = [], + driveFiles = [], + ) => { + queryClient.invalidateQueries({ queryKey: queryKeys.projects.all }); + + if (pendingPdfs.length > 0 || pendingRefs.length > 0 || driveFiles.length > 0) { + projectStore.setPendingProjectData(newProject.id, { pendingPdfs, pendingRefs, driveFiles }); + } + + setShowCreateForm(false); + navigate(`/projects/${newProject.id}`); + }; + + const openProject = projectId => { + navigate(`/projects/${projectId}`); + }; + + const handleDeleteProject = async targetProjectId => { + const confirmed = await confirmDialog.open({ + title: 'Delete Project', + description: + 'Are you sure you want to delete this entire project? This action cannot be undone.', + confirmText: 'Delete Project', + variant: 'danger', + }); + if (!confirmed) return; + + const project = projects()?.find(p => p.id === targetProjectId); + if (!project?.orgId) { + showToast.error('Error', 'Unable to find project organization'); + return; + } + + try { + const response = await fetch( + `${API_BASE}/api/orgs/${project.orgId}/projects/${targetProjectId}`, + { method: 'DELETE', credentials: 'include' }, + ); + if (!response.ok) { + const data = await response.json().catch(() => ({})); + throw new Error(data.error || 'Failed to delete project'); + } + + queryClient.invalidateQueries({ queryKey: queryKeys.projects.all }); + showToast.success('Project Deleted', 'The project has been deleted successfully'); + } catch (err) { + const { handleError } = await import('@/lib/error-utils.js'); + await handleError(err, { toastTitle: 'Delete Failed' }); + } + }; + + const isLoading = () => projectListQuery.isInitialLoading(); + const hasProjects = () => projects()?.length > 0; + + const handleCreateClick = () => { + if (props.onCreateClick) { + props.onCreateClick(); + } else { + setShowCreateForm(true); + } + }; + + return ( +
+ {/* Header */} + +
+

+ Your Projects +

+ + + + } + > + + +
+
+ + {/* Create form */} + +
+ setShowCreateForm(false)} + /> +
+
+ + {/* Projects grid */} +
+ {/* Loading skeletons */} + + + + + + + {/* Empty state */} + + + + + {/* Project cards */} + + {(project, index) => ( + + )} + +
+ + +
+ ); +} + +export default ProjectsSection; diff --git a/packages/web/src/components/dashboard/QuickActions.jsx b/packages/web/src/components/dashboard/QuickActions.jsx new file mode 100644 index 000000000..0875f6d59 --- /dev/null +++ b/packages/web/src/components/dashboard/QuickActions.jsx @@ -0,0 +1,110 @@ +/** + * QuickActions - Quick start cards for creating new appraisals + */ + +import { For } from 'solid-js'; +import { FiPlayCircle, FiBook } from 'solid-icons/fi'; + +/** + * Quick action button component + * @param {Object} props + * @param {string} props.title - Action title + * @param {string} props.description - Action description + * @param {JSX.Element} props.icon - Icon element + * @param {string} props.iconBg - Background class for icon + * @param {string} props.border - Border color class + * @param {() => void} props.onClick - Click handler + * @param {boolean} [props.disabled] - Whether action is disabled + * @param {number} [props.delay] - Animation delay + */ +function QuickActionCard(props) { + return ( + + ); +} + +/** + * Quick actions section + * @param {Object} props + * @param {() => void} props.onStartROBINSI - Handler for starting ROBINS-I + * @param {() => void} props.onStartAMSTAR2 - Handler for starting AMSTAR 2 + * @param {() => void} props.onLearnMore - Handler for learn more action + * @param {boolean} [props.canCreate] - Whether user can create new appraisals + */ +export function QuickActions(props) { + const actions = () => [ + { + id: 'robins-i', + title: 'Start ROBINS-I', + description: 'Risk of bias for non-randomized studies', + icon: , + iconBg: 'bg-blue-50', + border: 'border-blue-100 hover:border-blue-200', + onClick: () => props.onStartROBINSI?.(), + delay: 0, + requiresCreate: true, + }, + { + id: 'amstar-2', + title: 'Start AMSTAR 2', + description: 'Quality assessment for systematic reviews', + icon: , + iconBg: 'bg-emerald-50', + border: 'border-emerald-100 hover:border-emerald-200', + onClick: () => props.onStartAMSTAR2?.(), + delay: 50, + requiresCreate: true, + }, + { + id: 'learn-more', + title: 'Learn More', + description: 'View documentation and guides', + icon: , + iconBg: 'bg-violet-50', + border: 'border-violet-100 hover:border-violet-200', + onClick: () => props.onLearnMore?.(), + delay: 100, + requiresCreate: false, + }, + ]; + + return ( +
+

Quick Start

+
+ + {action => ( + + )} + +
+
+ ); +} + +export default QuickActions; diff --git a/packages/web/src/components/dashboard/StatsRow.jsx b/packages/web/src/components/dashboard/StatsRow.jsx new file mode 100644 index 000000000..21cb3e1b5 --- /dev/null +++ b/packages/web/src/components/dashboard/StatsRow.jsx @@ -0,0 +1,105 @@ +/** + * StatsRow - Row of stat cards showing key metrics + */ + +import { Show, For } from 'solid-js'; +import { FiFolder, FiCheck, FiFileText, FiUsers } from 'solid-icons/fi'; + +/** + * Individual stat card + * @param {Object} props + * @param {string} props.label - Stat label + * @param {string|number} props.value - Main value to display + * @param {string} [props.subtext] - Optional subtext + * @param {JSX.Element} props.icon - Icon element + * @param {string} [props.iconBg] - Background class for icon container + * @param {number} [props.delay] - Animation delay in ms + */ +export function StatCard(props) { + return ( +
+
+
+

{props.label}

+

{props.value}

+
+
+ {props.icon} +
+
+ +

{props.subtext}

+
+
+ ); +} + +/** + * Stats row with computed metrics + * @param {Object} props + * @param {number} props.projectCount - Number of active projects + * @param {number} props.completedStudies - Number of completed studies + * @param {number} props.totalStudies - Total number of studies + * @param {number} props.localAppraisalCount - Number of local appraisals + */ +export function StatsRow(props) { + const stats = () => [ + { + label: 'Active Projects', + value: props.projectCount, + icon: , + iconBg: 'bg-blue-50', + delay: 0, + }, + { + label: 'Studies Reviewed', + value: props.completedStudies, + subtext: props.totalStudies > 0 ? `of ${props.totalStudies} total` : undefined, + icon: , + iconBg: 'bg-emerald-50', + delay: 50, + }, + { + label: 'Local Appraisals', + value: props.localAppraisalCount, + icon: , + iconBg: 'bg-amber-50', + delay: 100, + }, + { + label: 'Team Members', + value: props.teamMemberCount || '-', + subtext: props.teamMemberCount ? 'Across all projects' : undefined, + icon: , + iconBg: 'bg-violet-50', + delay: 150, + }, + ]; + + return ( +
+ + {stat => ( + + )} + +
+ ); +} + +export default StatsRow; diff --git a/packages/web/src/components/dashboard/index.js b/packages/web/src/components/dashboard/index.js new file mode 100644 index 000000000..0531abbf0 --- /dev/null +++ b/packages/web/src/components/dashboard/index.js @@ -0,0 +1,18 @@ +/** + * Dashboard Module + * + * Modern dashboard with clean design based on Linear/Notion aesthetic. + * Handles all user states: logged out, loading, active, quota exceeded. + */ + +export { default } from './Dashboard.jsx'; +export { DashboardHeader } from './DashboardHeader.jsx'; +export { StatsRow, StatCard } from './StatsRow.jsx'; +export { ProgressCard } from './ProgressCard.jsx'; +export { QuickActions } from './QuickActions.jsx'; +export { ActivityFeed } from './ActivityFeed.jsx'; +export { DashboardSkeleton } from './DashboardSkeleton.jsx'; +export { ProjectCard } from './ProjectCard.jsx'; +export { ProjectsSection } from './ProjectsSection.jsx'; +export { LocalAppraisalCard } from './LocalAppraisalCard.jsx'; +export { LocalAppraisalsSection } from './LocalAppraisalsSection.jsx'; diff --git a/packages/web/src/global.css b/packages/web/src/global.css index ff00f0330..082ebf0d5 100644 --- a/packages/web/src/global.css +++ b/packages/web/src/global.css @@ -214,3 +214,26 @@ background: rgba(0, 100, 255, 0.3) !important; color: transparent; } + +/* Dashboard animations */ +@keyframes fade-up { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes stat-rise { + from { + opacity: 0; + transform: translateY(8px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} From 2b895f06db49ac616702ab056138e9354c5324c8 Mon Sep 17 00:00:00 2001 From: Jacob Maynard Date: Fri, 9 Jan 2026 22:29:27 -0600 Subject: [PATCH 04/15] UI style updates and routing --- packages/web/src/Routes.jsx | 2 +- .../checklist/CreateLocalChecklist.jsx | 5 ++++- .../checklist/LocalChecklistView.jsx | 5 +++-- .../src/components/dashboard/Dashboard.jsx | 21 +++++++++++++----- .../components/dashboard/DashboardHeader.jsx | 22 +++++++++++++------ .../dashboard/LocalAppraisalCard.jsx | 7 +++--- .../dashboard/LocalAppraisalsSection.jsx | 4 ++-- .../src/components/dashboard/ProjectCard.jsx | 2 +- .../components/dashboard/ProjectsSection.jsx | 7 +++--- .../src/components/dev/DevImportProject.jsx | 2 +- 10 files changed, 50 insertions(+), 27 deletions(-) diff --git a/packages/web/src/Routes.jsx b/packages/web/src/Routes.jsx index a3c8457d6..826e1e753 100644 --- a/packages/web/src/Routes.jsx +++ b/packages/web/src/Routes.jsx @@ -122,7 +122,7 @@ export default function AppRoutes() { {/* Local checklists (not org-scoped, work offline) */} - + {/* Mock routes - public, visual-only wireframes */} diff --git a/packages/web/src/components/checklist/CreateLocalChecklist.jsx b/packages/web/src/components/checklist/CreateLocalChecklist.jsx index 946b2f13d..3487be8e1 100644 --- a/packages/web/src/components/checklist/CreateLocalChecklist.jsx +++ b/packages/web/src/components/checklist/CreateLocalChecklist.jsx @@ -18,12 +18,15 @@ export default function CreateLocalChecklist() { const { createChecklist, savePdf } = localChecklistsStore; const [name, setName] = createSignal(''); - const [checklistType, setChecklistType] = createSignal(DEFAULT_CHECKLIST_TYPE); + const [checklistType, setChecklistType] = createSignal( + searchParams.type || DEFAULT_CHECKLIST_TYPE, + ); const [pdfFile, setPdfFile] = createSignal(null); const [creating, setCreating] = createSignal(false); const [error, setError] = createSignal(null); const typeOptions = getChecklistTypeOptions(); + console.log('typeOptions', typeOptions); const handleFilesChange = async files => { const file = files[0]; diff --git a/packages/web/src/components/checklist/LocalChecklistView.jsx b/packages/web/src/components/checklist/LocalChecklistView.jsx index bffa5a724..2065a6b3e 100644 --- a/packages/web/src/components/checklist/LocalChecklistView.jsx +++ b/packages/web/src/components/checklist/LocalChecklistView.jsx @@ -8,7 +8,7 @@ import { createSignal, createEffect, Show, onCleanup, createMemo } from 'solid-js'; import { debounce } from '@solid-primitives/scheduled'; -import { useParams, useNavigate } from '@solidjs/router'; +import { useParams, useNavigate, useSearchParams } from '@solidjs/router'; import ChecklistWithPdf from '@/components/checklist/ChecklistWithPdf.jsx'; import CreateLocalChecklist from '@/components/checklist/CreateLocalChecklist.jsx'; import localChecklistsStore from '@/stores/localChecklistsStore'; @@ -19,6 +19,7 @@ import ScoreTag from '@/components/checklist/ScoreTag.jsx'; export default function LocalChecklistView() { const params = useParams(); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const { getChecklist, updateChecklist, getPdf, savePdf, deletePdf } = localChecklistsStore; const [checklist, setChecklist] = createSignal(null); @@ -174,7 +175,7 @@ export default function LocalChecklistView() { ); return ( - }> + }> { // Trigger the project creation modal through the ProjectsPanel // For now, just scroll to projects section document.getElementById('projects-section')?.scrollIntoView({ behavior: 'smooth' }); + setShowCreateForm(true); }; const handleStartROBINSI = () => { - navigate('/checklist/new?type=robins-i'); + navigate('/checklist?type=ROBINS_I'); }; const handleStartAMSTAR2 = () => { - navigate('/checklist/new?type=amstar-2'); + navigate('/checklist?type=AMSTAR2'); }; const handleLearnMore = () => { - window.open('https://docs.corates.app', '_blank'); + window.location.href = `${LANDING_URL}/resources`; }; return ( @@ -200,7 +203,10 @@ export function Dashboard() {
{/* Left column - Projects and Local appraisals */}
- +
@@ -243,7 +249,10 @@ export function Dashboard() { {/* Still show content */}
- +
diff --git a/packages/web/src/components/dashboard/DashboardHeader.jsx b/packages/web/src/components/dashboard/DashboardHeader.jsx index 59c928ce4..4914226f5 100644 --- a/packages/web/src/components/dashboard/DashboardHeader.jsx +++ b/packages/web/src/components/dashboard/DashboardHeader.jsx @@ -4,6 +4,7 @@ import { Show } from 'solid-js'; import { FiPlus, FiSearch } from 'solid-icons/fi'; +import { getRoleLabel } from '@/components/auth/RoleSelector.jsx'; /** * @param {Object} props @@ -16,19 +17,26 @@ import { FiPlus, FiSearch } from 'solid-icons/fi'; export function DashboardHeader(props) { const firstName = () => { const name = props.user?.name || ''; - return name.split(' ')[0] || 'there'; + return name.split(' ')[0] || ''; }; return (
-

Welcome back,

-

- {firstName()} -

- -

{props.user.email}

+ Welcome to CoRATES!

} + > +

Welcome back,

+

+ {firstName()} +

+
+ +

+ {props.user?.persona ? getRoleLabel(props.user.persona) : props.user.email} +

diff --git a/packages/web/src/components/dashboard/LocalAppraisalCard.jsx b/packages/web/src/components/dashboard/LocalAppraisalCard.jsx index 75499bab3..05c74d812 100644 --- a/packages/web/src/components/dashboard/LocalAppraisalCard.jsx +++ b/packages/web/src/components/dashboard/LocalAppraisalCard.jsx @@ -10,7 +10,7 @@ */ import { Show, createMemo } from 'solid-js'; -import { FiFileText, FiArrowRight, FiTrash2 } from 'solid-icons/fi'; +import { FiFileText, FiChevronRight, FiTrash2 } from 'solid-icons/fi'; import { Editable } from '@corates/ui'; import { getChecklistMetadata } from '@/checklist-registry'; @@ -108,10 +108,11 @@ export function LocalAppraisalCard(props) {
diff --git a/packages/web/src/components/dashboard/LocalAppraisalsSection.jsx b/packages/web/src/components/dashboard/LocalAppraisalsSection.jsx index fede5c2a2..e8c0a317c 100644 --- a/packages/web/src/components/dashboard/LocalAppraisalsSection.jsx +++ b/packages/web/src/components/dashboard/LocalAppraisalsSection.jsx @@ -51,7 +51,7 @@ function EmptyLocalState(props) { function SignInPrompt(props) { return (
@@ -150,7 +150,7 @@ export function LocalAppraisalsSection(props) {