Skip to main content

Overview

The Dashboard provides a comprehensive view of your job search performance with real-time metrics, trend analysis, and activity tracking.
All dashboard data is computed server-side via /api/analytics/dashboard and cached for performance.

Layout

The dashboard uses a two-column layout:

Main Content

  • Stat cards (4 key metrics)
  • Applications over time chart
  • Metrics grid (response rate, sources)

Activity Panel

  • Recent application activity
  • Status change notifications
  • Quick action links

Stat Cards

Four key metrics are displayed at the top with week-over-week trends:
Total Applications tracks all jobs where status is applied, interview, offer, or rejected.
// Stat card configuration (src/components/dashboard/stat-cards.tsx:12-19)
{
  key: 'totalApplications',
  changeKey: 'applicationsChange',
  label: 'Total Applications',
  icon: <BarChart3 className="w-3.5 h-3.5 text-terminal-500" />,
  statusColor: 'bg-terminal-500',
}
Calculation:
  • Count all jobs with applied_at timestamp
  • Compare to previous week’s count
  • Display as integer with +X% or -X% change

Stat Card Features

Each stat shows a week-over-week change with visual indicator:
// Trend display (src/components/dashboard/stat-cards.tsx:76-84)
<div className={cn(
  'flex items-center gap-1',
  isPositive ? 'text-terminal-500' : 'text-red-400'
)}>
  <span>{isPositive ? '▲' : '▼'}</span>
  <span>{isPositive ? '+' : ''}{change.toFixed(1)}%</span>
  <span className="text-muted-foreground">vs last week</span>
</div>
  • Green ▲ — Metric improved
  • Red ▼ — Metric declined
Each card includes a mini sparkline chart showing the 7-day trend:
// Sparkline component (src/components/dashboard/sparkline.tsx)
<Sparkline
  data={[65, 72, 68, 75, 82, 79, 85]}
  color={isPositive ? 'hsl(0 0% 75%)' : 'hsl(0 65% 58%)'}
/>
Each card has a small colored square in the top-right corner indicating metric health:
  • Green — Positive trend
  • Red — Negative trend
  • Color-coded by metric type (terminal-500, blue, purple, yellow)

Applications Chart

The main chart displays your application volume over time with status breakdowns:
1

Time Series Data

Data is aggregated by day for the last 30 days:
// Time series data structure (src/lib/types/dashboard.ts:33-37)
export interface TimeSeriesData {
  date: string;      // "2026-03-01"
  value: number;     // Total applications on this date
  saved?: number;    // Jobs saved
  applied?: number;  // Jobs applied
  interview?: number;
  offer?: number;
  rejected?: number;
}
2

Multi-Series Chart

The chart component supports overlaying multiple status series:
// Chart usage (src/views/app/dashboard.tsx:77-80)
<ApplicationsChart 
  data={data.applicationsOverTime}
  statusData={data.statusOverTime}
/>
3

Interactive Legend

Click legend items to toggle series visibility and isolate specific statuses.
The chart uses Recharts or similar library for responsive, accessible visualizations.

Metrics Grid

Below the main chart, the metrics grid displays secondary KPIs:

Response Rate

Response Rate = (interviews + offers) / applied * 100%Measures how often you hear back from applications.

Avg Response Time

Avg Response Time in days between applied_at and interview_at or rejected_at.Helps set expectations for follow-ups.

Source Breakdown

By Source shows application distribution:
  • BrighterMonday: 45%
  • Fuzu: 30%
  • LinkedIn: 15%
  • Manual: 10%

Metrics Grid Implementation

// Metrics grid component (src/views/app/dashboard.tsx:83-87)
<MetricsGrid 
  responseRate={data.responseRate}
  avgResponseTime={data.avgResponseTime}
  bySource={data.bySource}
/>
interface MetricsGridProps {
  responseRate: number;      // 0-100 percentage
  avgResponseTime: number;   // days (e.g., 7.5)
  bySource: SourceBreakdown[];
}

Activity Panel

The right sidebar displays a chronological activity feed:
Five activity types are tracked:
// Activity types (src/lib/types/dashboard.ts:52-65)
export interface ActivityItem {
  id: string;
  type: 'application' | 'interview' | 'status_change' | 'viewed' | 'match';
  title: string;
  description: string;
  timestamp: string;    // ISO 8601
  action?: {
    label: string;
    href?: string;      // Link to job drawer or external page
    onClick?: () => void;
  };
}
Icon Mapping:
  • application → Send icon
  • interview → Mic2 icon
  • status_change → RefreshCw icon
  • viewed → Eye icon
  • match → Target icon

Toggle Activity Panel

Click HIDE_ACTIVITY / SHOW_ACTIVITY button to collapse the sidebar:
// Toggle implementation (src/views/app/dashboard.tsx:14-16, 49-68)
const [showActivity, setShowActivity] = React.useState(true);

<Button onClick={() => setShowActivity(!showActivity)}>
  {showActivity ? (
    <><PanelLeft /> HIDE_ACTIVITY</>
  ) : (
    <><PanelRight /> SHOW_ACTIVITY</>
  )}
</Button>

{showActivity && (
  <div className="w-full lg:w-80">
    <ActivityPanel activities={data.activities} />
  </div>
)}

Data Structure

DashboardStats Type

// Complete dashboard data type (src/lib/types/dashboard.ts:3-31)
export interface DashboardStats {
  // Overview metrics
  totalApplications: number;
  applicationsChange: number; // percentage
  
  activeCompanies: number;
  companiesChange: number;
  
  interviewRate: number; // percentage
  interviewRateChange: number;
  
  avgMatchScore: number;
  matchScoreChange: number;

  // Time series data
  applicationsOverTime: TimeSeriesData[];
  statusOverTime: StatusTimeSeries[];

  // Breakdown
  byStatus: Record<JobStatus, number>;
  bySource: SourceBreakdown[];
  
  // Response metrics
  responseRate: number;
  avgResponseTime: number; // days

  // Activities
  activities: ActivityItem[];
}

API Endpoint

GET /api/analytics/dashboard

Fetches all dashboard metrics for the authenticated user.
period
string
default:"30d"
Time period for metrics: 7d, 30d, 90d, or all
Response:
{
  "totalApplications": 42,
  "applicationsChange": 12.5,
  "activeCompanies": 18,
  "companiesChange": 8.3,
  "interviewRate": 23.8,
  "interviewRateChange": -2.1,
  "avgMatchScore": 78.4,
  "matchScoreChange": 5.2,
  "applicationsOverTime": [
    { "date": "2026-03-01", "value": 5, "applied": 3, "interview": 2 },
    { "date": "2026-03-02", "value": 7, "applied": 5, "interview": 1, "offer": 1 }
  ],
  "bySource": [
    { "source": "brightermonday", "count": 19, "percentage": 45.2 },
    { "source": "linkedin", "count": 12, "percentage": 28.6 }
  ],
  "responseRate": 31.5,
  "avgResponseTime": 8.2,
  "activities": []
}
// React Query hook (src/hooks/use-dashboard.ts)
import { useQuery } from '@tanstack/react-query'

export function useDashboard() {
  return useQuery({
    queryKey: ['dashboard'],
    queryFn: async () => {
      const res = await fetch('/api/analytics/dashboard')
      if (!res.ok) throw new Error('Failed to fetch dashboard')
      return res.json() as Promise<DashboardStats>
    },
    staleTime: 5 * 60 * 1000, // 5 minutes
  })
}

Loading States

The dashboard displays a branded loading animation while fetching data:
// Loading screen (src/views/app/dashboard.tsx:18-27)
if (isLoading) {
  return (
    <div className="flex items-center justify-center h-[60vh]">
      <div className="flex flex-col items-center gap-4">
        <div className="w-12 h-12 border-4 border-terminal-500/20 border-t-terminal-500 rounded-full animate-spin" />
        <div className="text-terminal-500 font-mono text-sm animate-pulse">
          SYNCING_METRICS...
        </div>
      </div>
    </div>
  );
}

Empty State

If no data exists, display a helpful empty state:
// Empty state (src/views/app/dashboard.tsx:29-42)
if (!data) {
  return (
    <PageWrapper title="Dashboard" description="Track your job search metrics and progress">
      <div className="glass rounded-lg p-12 text-center">
        <p className="text-muted-foreground font-mono">NO_DATA_AVAILABLE</p>
        <p className="text-sm text-muted-foreground mt-2 font-mono opacity-50">
          START_TRACKING_JOBS_TO_SEE_METRICS
        </p>
      </div>
    </PageWrapper>
  );
}

Performance Optimization

All metrics are computed in /api/analytics/dashboard using SQL aggregations for speed:
-- Example: Interview rate calculation
SELECT 
  COUNT(*) FILTER (WHERE status IN ('interview', 'offer')) * 100.0 / 
  COUNT(*) FILTER (WHERE status != 'saved') AS interview_rate
FROM jobs
WHERE user_id = $1 AND deleted_at IS NULL;
Dashboard data is cached with a 5-minute staleTime in React Query to avoid redundant fetches.
The activity panel loads separately from main metrics to prioritize above-the-fold content.

Next Steps

Intel

Get AI-powered insights and recommendations

Tracker

Manage your job applications

Analytics API

API reference for dashboard data

Export Data

Export your metrics as CSV or JSON

Build docs developers (and LLMs) love