Skip to content

ProjectStats

Tagged union of platform-specific stats. Each variant includes a type discriminator field that identifies the source platform.

Definition

tsx
interface GitHubStats {
  stars?: number
  forks?: number
}

interface NpmStats {
  downloads?: string
  version?: string
}

interface ProductHuntStats {
  upvotes?: number
  comments?: number
  launchDate?: string
}

interface YouTubeStats {
  subscribers?: number
  views?: number
  latestVideoTitle?: string | null
  latestVideoUrl?: string | null
  latestVideoPublishedAt?: string | null
}

interface GumroadStats {
  formattedRevenue?: string
  salesCount?: number
  subscriberCount?: number
}

interface LemonSqueezyStats {
  formattedMRR?: string
  orderCount?: number
  customerCount?: number
}

interface DevToStats {
  articleCount?: number
  totalViews?: number
  totalReactions?: number
}

type ProjectStats =
  | ({ type: 'github' } & GitHubStats)
  | ({ type: 'manual' } & Record<string, never>)
  | ({ type: 'npm' } & NpmStats)
  | ({ type: 'product-hunt' } & ProductHuntStats)
  | ({ type: 'youtube' } & YouTubeStats)
  | ({ type: 'gumroad' } & GumroadStats)
  | ({ type: 'lemonsqueezy' } & LemonSqueezyStats)
  | ({ type: 'devto' } & DevToStats)
  | ({ type: 'hybrid' } & GitHubStats & NpmStats)

The type field acts as a discriminator, enabling TypeScript to narrow the available properties via control flow analysis.

All Properties

GitHub Stats

PropertyTypeSourceDescription
starsnumberGitHub APIRepository star count
forksnumberGitHub APIRepository fork count

Available for: github, hybrid

NPM Stats

PropertyTypeSourceDescription
downloadsstringnpm Downloads APIMonthly download count (as string)
versionstringnpm RegistryLatest published version

Available for: npm, hybrid

Product Hunt Stats

PropertyTypeSourceDescription
upvotesnumberProduct Hunt APITotal upvotes
commentsnumberProduct Hunt APITotal comments
launchDatestringProduct Hunt APIDate featured (ISO 8601)

Available for: product-hunt

YouTube Stats

PropertyTypeSourceDescription
subscribersnumberYouTube Data API v3Channel subscriber count
viewsnumberYouTube Data API v3Total channel view count
latestVideoTitlestring | nullYouTube Data API v3Title of the most recent upload
latestVideoUrlstring | nullYouTube Data API v3URL of the most recent upload
latestVideoPublishedAtstring | nullYouTube Data API v3Publish date of the most recent upload

Available for: youtubeRequires: YOUTUBE_TOKEN environment variable

Gumroad Stats

PropertyTypeSourceDescription
formattedRevenuestringGumroad APIFormatted revenue string (e.g., "$1,234.56")
salesCountnumberGumroad APITotal number of sales
subscriberCountnumberGumroad APITotal number of subscribers

Available for: gumroadRequires: GUMROAD_TOKEN environment variable

Lemon Squeezy Stats

PropertyTypeSourceDescription
formattedMRRstringLemon Squeezy APIFormatted Monthly Recurring Revenue (e.g., "$1,234.56")
orderCountnumberLemon Squeezy APITotal number of orders
customerCountnumberLemon Squeezy APITotal number of customers

Available for: lemonsqueezyRequires: LS_TOKEN environment variable

Dev.to Stats

PropertyTypeSourceDescription
articleCountnumberDev.to APITotal number of articles published
totalViewsnumberDev.to APISum of page views across all articles
totalReactionsnumberDev.to APITotal reactions across all articles

Available for: devtoRequires: No authentication (public API). Optional: DEV_TO_API_KEY env var enables page_views_count for accurate totalViews.

Stats by Project Type

TypeGitHub StatsNPM StatsPH StatsYouTube StatsGumroad StatsLS StatsDevTo Stats
github
npm
hybrid
product-hunt
youtube
gumroad
lemonsqueezy
devto
manual

Usage

tsx
import type { ProjexProject } from '@manningworks/projex'

function formatStats(project: ProjexProject): string[] {
  const stats: string[] = []
  
  if (!project.stats) return stats

  if (project.stats.type === 'github' || project.stats.type === 'hybrid') {
    if (project.stats.stars) stats.push(`${project.stats.stars} stars`)
    if (project.stats.forks) stats.push(`${project.stats.forks} forks`)
  }
  
  if (project.stats.type === 'npm' || project.stats.type === 'hybrid') {
    if (project.stats.downloads) stats.push(`${project.stats.downloads} downloads`)
  }

  if (project.stats.type === 'youtube') {
    if (project.stats.subscribers) stats.push(`${project.stats.subscribers} subscribers`)
  }

  if (project.stats.type === 'gumroad') {
    if (project.stats.formattedRevenue) stats.push(`${project.stats.formattedRevenue} revenue`)
  }

  if (project.stats.type === 'devto') {
    if (project.stats.articleCount) stats.push(`${project.stats.articleCount} articles`)
  }
  
  return stats
}

Null Safety

The stats property on ProjexProject can be null:

tsx
if (project.stats) {
  // Safe to access stats properties
  console.log(project.stats.stars)
}

Manual projects that don't provide explicit stats will have stats: null.

Normalization

Use normaliseStats to format all stats for display:

tsx
import { normaliseStats } from '@manningworks/projex'

const formattedStats = normaliseStats(project.stats || {}, project.type)
// [{ label: 'Stars', value: '1.2K' }, { label: 'Revenue', value: '$1,234.56' }, ...]

See normaliseStats for all output labels.

Manual Stats Override

For manual project types, you can provide any stats values directly. The type field is set automatically during normalisation:

tsx
{
  id: 'client-project',
  type: 'manual',
  status: 'shipped',
  name: 'Client Website',
  stats: {
    type: 'manual',
    downloads: '5000',
    version: '2.1.0',
  }
}

For fetched types, input stats override values are merged with fetched data (input values take precedence). The type field is set automatically by normalise based on the project type.