Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
71c1b20
feat: add sefaria API endpoint normalization util
Jul 28, 2026
2775849
feat: add per-project deduped endpoint usage counting with top-6+Othe…
Jul 28, 2026
65b3b7a
feat: add pie chart of most-used Sefaria API endpoints
Jul 28, 2026
c37f4b9
fix: harden tool usage counting against bad data, unhide charts on em…
Jul 28, 2026
bb30b68
feat: add vibe-coded vs not-vibe-coded monthly trend util
Jul 28, 2026
f5f8a40
feat: add vibe-coded vs not-vibe-coded trend chart
Jul 28, 2026
5f66e07
fix: dedupe vibe-coded series constant and add trend-artifact caption
Jul 28, 2026
65e5969
feat: add technologies-used frequency chart
Jul 28, 2026
0fa2faa
feat: give each category label a distinct pale, muted color
Jul 28, 2026
39c5f5e
feat: widen and left-align the search bar under the developers logo
Jul 28, 2026
dbdf28c
fix: force every technology label to render on the Technologies used …
Jul 28, 2026
2fea767
refactor: split each chart into its own component under charts/
Aug 3, 2026
5bfba95
chore: ignore local data/ dumps to prevent re-committing sensitive CSVs
Aug 4, 2026
93a77f0
feat: add getScreenshotUrl utility backed by a screenshot manifest
Jul 30, 2026
f3733d4
feat: layer screenshot background + scrim into ProjectCard
Jul 30, 2026
1038bc2
feat: add npm run screenshots to pre-generate project website screens…
Jul 30, 2026
b90b9e6
docs: document npm run screenshots
Jul 30, 2026
aa86832
fix: address final review findings (stale test assertion, hover state…
Jul 30, 2026
c8e792c
feat: replace whole-card link with a front/back flip card (title-only…
Jul 30, 2026
b0db9ab
fix: disable pointer-events on hidden card face, add title hover color
Jul 30, 2026
77f802b
fix: hide flipped front-face link from keyboard/a11y tree, prevent fl…
Jul 30, 2026
1fa4cea
chore: ignore .worktrees/ directory used for isolated plan execution
Aug 5, 2026
8fa4a4d
feat: add generic BarChart chart-type component
Aug 5, 2026
472c5bd
refactor: rebuild SubmissionsTrendChart on generic BarChart
Aug 5, 2026
5fa2cb4
refactor: rebuild TechUsedChart on generic BarChart
Aug 5, 2026
7c5add9
refactor: rebuild KeywordFrequencyChart on generic BarChart
Aug 5, 2026
bb3f245
feat: add generic LineChart chart-type component
Aug 5, 2026
c8349b2
refactor: rebuild ExperienceTrendChart on generic LineChart
Aug 5, 2026
4137d33
refactor: rebuild VibeCodedTrendChart on generic LineChart
Aug 5, 2026
2e7baa9
feat: add generic PieChart chart-type component
Aug 5, 2026
5b871dc
refactor: rebuild ToolUsagePieChart on generic PieChart
Aug 5, 2026
be93ffe
fix: define --chart-* CSS palette so chart color references resolve
Aug 5, 2026
477f0b9
remove local screenshot feature, pending cloud-backed replacement
Aug 5, 2026
5d7f419
finish removing screenshot feature: drop puppeteer, ProjectCard wirin…
Aug 5, 2026
f01c4b1
refactor: migrate category/pill colors to CSS custom properties
Aug 5, 2026
9b1cdd1
fix: update relative imports for test files moved into __tests__/
Aug 5, 2026
cb58982
docs: add explanatory comments to submissionsTrend.js and its tests
Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ dist-ssr
*.local
.superpowers/
docs/superpowers/
.worktrees/

# Raw local data dumps (may contain sensitive info, never commit)
data/

# Editor directories and files
.vscode/*
Expand Down
117 changes: 27 additions & 90 deletions src/components/ChartsAndAnalytics.jsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
import { useEffect, useState } from 'react'
import {
Bar,
BarChart,
CartesianGrid,
Legend,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts'
import { fetchProjects } from '../data/fetchProjects.js'
import { getSubmissionsMonthlyTrend, getSubmissionsTrendByExperience } from '../utils/submissionsTrend.js'
import {
getSubmissionsMonthlyTrend,
getSubmissionsTrendByExperience,
getSubmissionsTrendByVibeCoded,
} from '../utils/submissionsTrend.js'
import { getKeywordCounts } from '../utils/keywords.js'
import { EXPERIENCE_LEVELS } from '../utils/experience.js'

const EXPERIENCE_COLORS = {
'No Experience': '#2a78d6',
Beginner: '#eb6834',
Intermediate: '#1baf7a',
Advanced: '#4a3aa7',
}
import { getToolUsageCounts } from '../utils/sefariaTools.js'
import { getTechCounts } from '../utils/techUsed.js'
import SubmissionsTrendChart from './charts/SubmissionsTrendChart.jsx'
import KeywordFrequencyChart from './charts/KeywordFrequencyChart.jsx'
import ExperienceTrendChart from './charts/ExperienceTrendChart.jsx'
import ToolUsagePieChart from './charts/ToolUsagePieChart.jsx'
import VibeCodedTrendChart from './charts/VibeCodedTrendChart.jsx'
import TechUsedChart from './charts/TechUsedChart.jsx'

function ChartsAndAnalytics() {
const [trend, setTrend] = useState(null)
const [keywordCounts, setKeywordCounts] = useState(null)
const [experienceTrend, setExperienceTrend] = useState(null)
const [toolUsage, setToolUsage] = useState(null)
const [vibeCodedTrend, setVibeCodedTrend] = useState(null)
const [techCounts, setTechCounts] = useState(null)
const [error, setError] = useState(null)

useEffect(() => {
Expand All @@ -35,83 +30,25 @@ function ChartsAndAnalytics() {
setTrend(getSubmissionsMonthlyTrend(projects))
setKeywordCounts(getKeywordCounts(projects))
setExperienceTrend(getSubmissionsTrendByExperience(projects))
setToolUsage(getToolUsageCounts(projects))
setVibeCodedTrend(getSubmissionsTrendByVibeCoded(projects))
setTechCounts(getTechCounts(projects))
})
.catch((err) => setError(err.message))
}, [])

if (error) return <p className="charts-and-analytics">Couldn't load chart data right now.</p>
if (!trend || !keywordCounts || !experienceTrend) return <p className="charts-and-analytics">Loading chart…</p>
if (experienceTrend.length === 0) {
return <p className="charts-and-analytics">No experience-level data available yet.</p>
if (!trend || !keywordCounts || !experienceTrend || !toolUsage || !vibeCodedTrend || !techCounts) {
return <p className="charts-and-analytics">Loading chart…</p>
}

return (
<div className="charts-and-analytics">
<h2>Submissions, past 12 months</h2>
<ResponsiveContainer width="100%" height={360}>
<BarChart data={trend}>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="month" />
<YAxis allowDecimals={false} />
<Tooltip />
<Bar dataKey="count" name="Submissions" fill="var(--accent)" />
</BarChart>
</ResponsiveContainer>

<h2>Keyword frequency</h2>
<ResponsiveContainer width="100%" height={keywordCounts.length * 28 + 40}>
<BarChart
data={keywordCounts}
layout="vertical"
margin={{ left: 24 }}
>
<CartesianGrid strokeDasharray="3 3" horizontal={false} />
<XAxis type="number" allowDecimals={false} />
<YAxis type="category" dataKey="keyword" width={100} />
<Tooltip />
<Bar dataKey="count" name="Projects" fill="var(--accent)" />
</BarChart>
</ResponsiveContainer>

<h2>Submissions by experience level</h2>
<ResponsiveContainer width="100%" height={360}>
<LineChart data={experienceTrend} margin={{ right: 100 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis dataKey="month" />
<YAxis allowDecimals={false} />
<Tooltip />
<Legend />
{EXPERIENCE_LEVELS.map((level) => (
<Line
key={level}
type="monotone"
dataKey={level}
name={level}
stroke={EXPERIENCE_COLORS[level]}
strokeWidth={2}
dot={{ r: 3 }}
// recharts 3.x gates the custom `label` render-prop behind an internal
// isAnimating flag cleared via onAnimationEnd, which doesn't fire
// reliably in this dev environment — without this, the end-of-line
// labels below silently never render (no test/lint failure to catch it).
isAnimationActive={false}
label={(props) =>
props.index === experienceTrend.length - 1 ? (
<text
x={props.x + 6}
y={props.y}
dy={4}
fill="var(--text-h)"
fontSize={12}
>
{level}
</text>
) : null
}
/>
))}
</LineChart>
</ResponsiveContainer>
<SubmissionsTrendChart data={trend} />
<KeywordFrequencyChart data={keywordCounts} />
<ExperienceTrendChart data={experienceTrend} />
<ToolUsagePieChart data={toolUsage} />
<VibeCodedTrendChart data={vibeCodedTrend} />
<TechUsedChart data={techCounts} />
</div>
)
}
Expand Down
1 change: 1 addition & 0 deletions src/components/Controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ function Controls({
<div className="dashboard-controls">
<input
type="text"
className="search-input"
placeholder="Search projects..."
value={searchText}
onChange={(event) => onSearchChange(event.target.value)}
Expand Down
60 changes: 38 additions & 22 deletions src/components/ProjectCard.jsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,44 @@
import { useState } from 'react'
import { getCategoryColor } from '../utils/categories.js'

function ProjectCard({ project }) {
const [flipped, setFlipped] = useState(false)

function handleCardClick() {
if (window.getSelection().toString()) return
setFlipped((current) => !current)
}

return (
<a
className="project-card"
href={project.project_link}
target="_blank"
rel="noreferrer"
>
{project.image_url && (
<img
className="project-card-image"
src={project.image_url}
alt={`${project.project_name} logo`}
/>
)}
<h3>{project.project_name}</h3>
<div className="project-card-categories">
{project.categories.map((category) => (
<span key={category} className="project-card-category">
{category}
</span>
))}
<div className="project-card" onClick={handleCardClick}>
<div className={`project-card-inner${flipped ? ' flipped' : ''}`}>
<div className="project-card-front">
<a
className="project-card-title"
Comment on lines +13 to +17
href={project.project_link}
target="_blank"
rel="noreferrer"
onClick={(event) => event.stopPropagation()}
>
{project.project_name}
</a>
</div>
<div className="project-card-back">
<div className="project-card-categories">
{project.categories.map((category) => (
<span
key={category}
className="project-card-category"
style={{ '--category-color': getCategoryColor(category) }}
>
{category}
</span>
))}
</div>
<p className="project-card-desc">{project.project_desc}</p>
</div>
</div>
<p className="project-card-desc">{project.project_desc}</p>
</a>
</div>
)
}

Expand Down
25 changes: 25 additions & 0 deletions src/components/charts/ExperienceTrendChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import LineChart from './types/LineChart.jsx'
import { EXPERIENCE_LEVELS } from '../../utils/experience.js'

const EXPERIENCE_COLORS = {
'No Experience': 'var(--chart-blue)',
Beginner: 'var(--chart-orange)',
Intermediate: 'var(--chart-aqua)',
Advanced: 'var(--chart-violet)',
}

function ExperienceTrendChart({ data }) {
return (
<LineChart
data={data}
title="Submissions by experience level"
series={EXPERIENCE_LEVELS.map((level) => ({
key: level,
name: level,
color: EXPERIENCE_COLORS[level],
}))}
/>
)
}

export default ExperienceTrendChart
17 changes: 17 additions & 0 deletions src/components/charts/KeywordFrequencyChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import BarChart from './types/BarChart.jsx'

function KeywordFrequencyChart({ data }) {
return (
<BarChart
data={data}
title="Keyword frequency"
dataKey="count"
categoryKey="keyword"
layout="vertical"
categoryWidth={100}
height={data.length * 28 + 40}
/>
)
}

export default KeywordFrequencyChart
15 changes: 15 additions & 0 deletions src/components/charts/SubmissionsTrendChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import BarChart from './types/BarChart.jsx'

function SubmissionsTrendChart({ data }) {
return (
<BarChart
data={data}
title="Submissions, past 12 months"
dataKey="count"
categoryKey="month"
barName="Submissions"
/>
)
}

export default SubmissionsTrendChart
17 changes: 17 additions & 0 deletions src/components/charts/TechUsedChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import BarChart from './types/BarChart.jsx'

function TechUsedChart({ data }) {
return (
<BarChart
data={data}
title="Technologies used"
dataKey="count"
categoryKey="label"
layout="vertical"
categoryWidth={180}
height={data.length * 28 + 40}
/>
)
}

export default TechUsedChart
32 changes: 32 additions & 0 deletions src/components/charts/ToolUsagePieChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import PieChart from './types/PieChart.jsx'

// Fixed-order categorical hues, pulled from the brand palette defined in
// index.css; gray is reserved for the "Other" bucket and is never one of
// the 6 identity colors.
const TOOL_SLICE_COLORS = [
'var(--chart-blue)',
'var(--chart-orange)',
'var(--chart-aqua)',
'var(--chart-yellow)',
'var(--chart-magenta)',
'var(--chart-green)',
]
const OTHER_SLICE_COLOR = 'var(--chart-neutral)'

function colorForToolSlice(entry, index) {
return entry.endpoint === 'Other' ? OTHER_SLICE_COLOR : TOOL_SLICE_COLORS[index]
}

function ToolUsagePieChart({ data }) {
return (
<PieChart
data={data}
title="Most-used Sefaria API endpoints"
dataKey="count"
nameKey="endpoint"
colorForSlice={colorForToolSlice}
/>
)
}

export default ToolUsagePieChart
24 changes: 24 additions & 0 deletions src/components/charts/VibeCodedTrendChart.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import LineChart from './types/LineChart.jsx'
import { VIBE_CODED_SERIES } from '../../utils/submissionsTrend.js'

const VIBE_CODED_COLORS = {
'Not vibe-coded': 'var(--chart-blue)',
'Vibe-coded': 'var(--chart-orange)',
}

function VibeCodedTrendChart({ data }) {
return (
<LineChart
data={data}
title="Vibe-coded vs. not, tracked since July 2026"
description={'"Vibe-coded" is a newly-tracked field, so earlier months may be undercounted or unreported rather than confirmed non-vibe-coded.'}
series={VIBE_CODED_SERIES.map((series) => ({
Comment on lines +11 to +15
key: series,
name: series,
color: VIBE_CODED_COLORS[series],
}))}
/>
)
}

export default VibeCodedTrendChart
Loading
Loading