From d01aae9a6b5f1a076d66632051ab99c25174d093 Mon Sep 17 00:00:00 2001 From: Sefaria Intern Date: Mon, 10 Aug 2026 10:21:04 +0300 Subject: [PATCH 1/3] Design feedback pass: subheader, controls layout, pie chart legend spacing Replaces the Developers logo with a Hebrew/English subheader block, widens and aligns the search bar and category toggle with the project grid, and adds breathing room between pie chart legend labels. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 1 + index.html | 2 +- src/Title.jsx | 16 ++-- src/components/Controls.jsx | 38 ++++----- .../charts/SubmissionsTrendChart.jsx | 2 +- src/components/charts/VibeCodedTrendChart.jsx | 2 +- src/components/charts/types/LineChart.jsx | 6 +- src/components/charts/types/PieChart.jsx | 2 +- src/data/fetchProjects.js | 25 ++++-- src/index.css | 78 +++++++++++++++---- src/utils/__tests__/submissionsTrend.test.js | 75 +++++++++--------- src/utils/submissionsTrend.js | 64 ++++++++------- src/utils/techUsed.js | 2 +- 13 files changed, 198 insertions(+), 115 deletions(-) diff --git a/.gitignore b/.gitignore index ae9a385..9228279 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ data/ *.njsproj *.sln *.sw? +/data/ diff --git a/index.html b/index.html index 0601b37..91a570f 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@ Powered by Sefaria Dashboard diff --git a/src/Title.jsx b/src/Title.jsx index 6eda98a..6c77b3f 100644 --- a/src/Title.jsx +++ b/src/Title.jsx @@ -1,12 +1,16 @@ -import developersLogo from './assets/developers_logo.png' - function Title() { return ( -
+

Powered by Sefaria Dashboard

- - Sefaria Developers logo - +
+

+ "אי אפשר לבית מדרש בלא חידוש" +

+

+ The center of all projects, websites, and apps created using Sefaria's open-source data and + technology +

+
) } diff --git a/src/components/Controls.jsx b/src/components/Controls.jsx index be36432..2e85baa 100644 --- a/src/components/Controls.jsx +++ b/src/components/Controls.jsx @@ -8,24 +8,26 @@ function Controls({ }) { return (
- onSearchChange(event.target.value)} - /> - +
+ onSearchChange(event.target.value)} + /> + +
{count} projects
) diff --git a/src/components/charts/SubmissionsTrendChart.jsx b/src/components/charts/SubmissionsTrendChart.jsx index 8fbb7b4..bbbcded 100644 --- a/src/components/charts/SubmissionsTrendChart.jsx +++ b/src/components/charts/SubmissionsTrendChart.jsx @@ -4,7 +4,7 @@ function SubmissionsTrendChart({ data }) { return ( - + {/* Legend's default itemSorter is 'value', which alphabetizes entries by + name — overriding the order series/ below are declared in (e.g. + "Not vibe-coded" would sort before "Vibe-coded"). Disable it to keep + declaration order instead. */} + {series.map(({ key, name, color }) => ( Data unavailable.

) : ( - + project.is_published && project.consent_to_display) .map((project) => ({ ...project, diff --git a/src/index.css b/src/index.css index 191cbac..1089a22 100644 --- a/src/index.css +++ b/src/index.css @@ -88,6 +88,33 @@ p { margin: 0; } +.app-header { + text-align: center; +} + +.app-subheader-block { + margin-top: 40px; + padding: 24px; + background: var(--accent-bg); + border-radius: 8px; + display: inline-block; +} + +.app-subheader { + font-family: 'Cardo', var(--heading); + font-size: 28px; + color: var(--text-h); + margin: 0 0 8px; +} + +.app-subheader-desc { + margin: 0 auto; + color: var(--text); + font-size: 20px; + line-height: 145%; + white-space: nowrap; +} + code, .counter { font-family: var(--mono); @@ -104,23 +131,22 @@ code { } .dashboard-controls { + display: flex; + flex-direction: column; + gap: 8px; + margin: 24px 0; + padding: 0 24px; +} + +.dashboard-controls-row { display: flex; gap: 12px; align-items: center; - justify-content: flex-start; - flex-wrap: wrap; - margin: 24px 0; - /* Anchors the row's left edge under the Developers logo's left edge. - The logo (646px, centered in the full-width header) and this row (inside - the sidebar-offset tab-content) live in different coordinate spaces, so - matching them means re-deriving the logo's position here: half the - leftover viewport width outside the logo, minus the sidebar's full - rendered width (220px content + 24px padding + 1px border-right). */ - margin-left: max(0px, calc((100vw - 646px) / 2 - 245px)); } .dashboard-controls input, .dashboard-controls select { + flex: 1; font: inherit; padding: 8px 12px; border: 1px solid var(--border); @@ -129,13 +155,18 @@ code { color: var(--text-h); } -.search-input { - width: 280px; +.dashboard-controls select { + appearance: none; + padding-right: 32px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23555' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 14px center; } .project-count { color: var(--text); font-size: 14px; + margin-left: 6px; } .project-grid { @@ -242,7 +273,7 @@ code { padding: 2px 8px; border-radius: 4px; background: color-mix(in srgb, var(--category-color, var(--accent)) 14%, var(--bg)); - color: color-mix(in srgb, var(--category-color, var(--accent)) 70%, var(--text-h)); + color: #000; border: 1px solid color-mix(in srgb, var(--category-color, var(--accent)) 40%, var(--bg)); } @@ -255,7 +286,22 @@ code { /* fills whatever vertical space the back face leaves after the category tags */ flex: 1; min-height: 0; - overflow-y: auto; + overflow-y: scroll; + scrollbar-color: color-mix(in srgb, var(--text) 55%, transparent) transparent; + scrollbar-width: thin; +} + +.project-card-desc::-webkit-scrollbar { + width: 8px; +} + +.project-card-desc::-webkit-scrollbar-thumb { + background-color: color-mix(in srgb, var(--text) 55%, transparent); + border-radius: 4px; +} + +.project-card-desc::-webkit-scrollbar-track { + background: transparent; } .app-body { @@ -310,3 +356,7 @@ code { flex-direction: column; gap: 48px; } + +.pie-chart .recharts-legend-item { + margin-right: 20px !important; +} diff --git a/src/utils/__tests__/submissionsTrend.test.js b/src/utils/__tests__/submissionsTrend.test.js index 9ee26db..4a2d7c2 100644 --- a/src/utils/__tests__/submissionsTrend.test.js +++ b/src/utils/__tests__/submissionsTrend.test.js @@ -3,23 +3,24 @@ import assert from 'node:assert/strict' import { getSubmissionsTrendByExperience, getSubmissionsTrendByVibeCoded } from '../submissionsTrend.js' // Fixed "today" for every test below, so results are predictable instead of -// depending on when the test happens to run. July 15 2026 in local time — -// JS Date months are 0-indexed, so 6 means July. -const referenceDate = new Date(2026, 6, 15) // July 2026 +// depending on when the test happens to run. August 15 2026 in local time — +// JS Date months are 0-indexed, so 7 means August. Charts only render data +// through the most recently *completed* month (July here), since August +// itself isn't over yet. +const referenceDate = new Date(2026, 7, 15) // August 2026 // --- getSubmissionsTrendByExperience ------------------------------------- // This function builds the data for ExperienceTrendChart: one row per // month, with a count for each experience level (No Experience, Beginner, // Intermediate, Advanced) in that month. -test('getSubmissionsTrendByExperience returns [] when no project has a parseable date', () => { - // Neither project has a real "Month Year" tag ("Date unspecified" and an - // empty tags array both fail to parse), so there's nothing to chart — - // the function should bail out to an empty array rather than a chart - // full of zeros. +test('getSubmissionsTrendByExperience returns [] when no project has a usable submission_date', () => { + // Neither project has a submission_date (null and missing both fail to + // parse), so there's nothing to chart — the function should bail out to + // an empty array rather than a chart full of zeros. const projects = [ - { tags: ['Date unspecified'], technical_experience: 'None' }, - { tags: [], technical_experience: 'Beginner' }, + { submission_date: null, technical_experience: 'None' }, + { technical_experience: 'Beginner' }, ] assert.deepEqual(getSubmissionsTrendByExperience(projects, referenceDate), []) }) @@ -30,7 +31,7 @@ test('getSubmissionsTrendByExperience spans from the earliest submission through // one month that has data — June has no submissions, so it's zero-filled // rather than skipped. const projects = [ - { tags: ['May 2026'], technical_experience: 'None' }, + { submission_date: '2026-05-10T10:00:00+00:00', technical_experience: 'None' }, ] const result = getSubmissionsTrendByExperience(projects, referenceDate) @@ -47,11 +48,11 @@ test('getSubmissionsTrendByExperience buckets each level independently per month // the others — e.g. May should show 1 "No Experience" AND 2 "Beginner" // in the same row, and July's counts shouldn't leak into May's. const projects = [ - { tags: ['May 2026'], technical_experience: 'None' }, - { tags: ['May 2026'], technical_experience: '<5 years' }, - { tags: ['May 2026'], technical_experience: '<5 years' }, - { tags: ['July 2026'], technical_experience: '5-10 years' }, - { tags: ['July 2026'], technical_experience: '10+ years' }, + { submission_date: '2026-05-01T10:00:00+00:00', technical_experience: 'None' }, + { submission_date: '2026-05-15T10:00:00+00:00', technical_experience: '<5 years' }, + { submission_date: '2026-05-20T10:00:00+00:00', technical_experience: '<5 years' }, + { submission_date: '2026-07-02T10:00:00+00:00', technical_experience: '5-10 years' }, + { submission_date: '2026-07-25T10:00:00+00:00', technical_experience: '10+ years' }, ] const result = getSubmissionsTrendByExperience(projects, referenceDate) @@ -62,13 +63,13 @@ test('getSubmissionsTrendByExperience buckets each level independently per month ]) }) -test('getSubmissionsTrendByExperience returns [] when every parseable project has unspecified experience', () => { +test('getSubmissionsTrendByExperience returns [] when every dated project has unspecified experience', () => { // The date parses fine, but technical_experience is blank, so // getExperienceLevel can't map it to any of the 4 known levels. With no // project contributing a real level, there's nothing meaningful to // chart, so this should behave the same as "no data" (empty array). const projects = [ - { tags: ['July 2026'], technical_experience: '' }, + { submission_date: '2026-07-10T10:00:00+00:00', technical_experience: '' }, ] const result = getSubmissionsTrendByExperience(projects, referenceDate) @@ -85,8 +86,8 @@ test('getSubmissionsTrendByExperience ignores unspecified-experience months when // on this exact behavior. const projects = [ // Unspecified experience, earlier date — should NOT push the chart's start back. - { tags: ['January 2026'], technical_experience: '' }, - { tags: ['July 2026'], technical_experience: 'None' }, + { submission_date: '2026-01-10T10:00:00+00:00', technical_experience: '' }, + { submission_date: '2026-07-10T10:00:00+00:00', technical_experience: 'None' }, ] const result = getSubmissionsTrendByExperience(projects, referenceDate) @@ -101,18 +102,18 @@ test('getSubmissionsTrendByExperience ignores unspecified-experience months when // at the earliest real data), with a count of "Vibe-coded" vs. // "Not vibe-coded" submissions per month. -test('getSubmissionsTrendByVibeCoded returns 12 zero-filled months when no project has a parseable date', () => { - const referenceDate = new Date(2026, 6, 15) // July 2026 +test('getSubmissionsTrendByVibeCoded returns 12 zero-filled months when no project has a usable submission_date', () => { + const referenceDate = new Date(2026, 7, 15) // August 2026 — most recently completed month is July // Neither project has a usable date, so every month should come back // zero-filled — but note this function always returns exactly 12 months // (the trailing year up to referenceDate) regardless of whether there's // any data, unlike getSubmissionsTrendByExperience above which returns [] // when there's nothing to show. - const projects = [{ tags: ['Date unspecified'] }, {}] + const projects = [{ submission_date: null }, {}] const result = getSubmissionsTrendByVibeCoded(projects, referenceDate) assert.equal(result.length, 12) - assert.equal(result[11].month, 'Jul 2026') // last of the 12 months is always referenceDate's month + assert.equal(result[11].month, 'Jul 2026') // last of the 12 months is always the most recently completed month for (const entry of result) { assert.equal(entry['Vibe-coded'], 0) assert.equal(entry['Not vibe-coded'], 0) @@ -120,15 +121,15 @@ test('getSubmissionsTrendByVibeCoded returns 12 zero-filled months when no proje }) test('getSubmissionsTrendByVibeCoded buckets each project into the correct series for its month', () => { - const referenceDate = new Date(2026, 6, 15) // July 2026 + const referenceDate = new Date(2026, 7, 15) // August 2026 — most recently completed month is July // 4 projects: 3 in July (2 vibe-coded, 1 not) and 1 in June (not // vibe-coded). Checks that each project's vibe_coded boolean routes it // into the right one of the two series, per month. const projects = [ - { tags: ['July 2026'], vibe_coded: true }, - { tags: ['July 2026'], vibe_coded: true }, - { tags: ['July 2026'], vibe_coded: false }, - { tags: ['June 2026'], vibe_coded: false }, + { submission_date: '2026-07-05T10:00:00+00:00', vibe_coded: true }, + { submission_date: '2026-07-12T10:00:00+00:00', vibe_coded: true }, + { submission_date: '2026-07-20T10:00:00+00:00', vibe_coded: false }, + { submission_date: '2026-06-15T10:00:00+00:00', vibe_coded: false }, ] const result = getSubmissionsTrendByVibeCoded(projects, referenceDate) @@ -139,15 +140,15 @@ test('getSubmissionsTrendByVibeCoded buckets each project into the correct serie assert.deepEqual(june, { month: 'Jun 2026', 'Vibe-coded': 0, 'Not vibe-coded': 1 }) }) -test('getSubmissionsTrendByVibeCoded ignores projects with unparseable or missing dates', () => { - const referenceDate = new Date(2026, 6, 15) // July 2026 - // 3 projects, only 1 with a real date ("July 2026"); the other 2 have an - // unparseable date string and no tags at all, respectively. Both bad - // ones should be silently dropped rather than counted or causing an - // error — so the total across all months should be exactly 1. +test('getSubmissionsTrendByVibeCoded ignores projects with a missing submission_date', () => { + const referenceDate = new Date(2026, 7, 15) // August 2026 — most recently completed month is July + // 3 projects, only 1 with a real submission_date; the other 2 have null + // and no submission_date field at all, respectively. Both bad ones should + // be silently dropped rather than counted or causing an error — so the + // total across all months should be exactly 1. const projects = [ - { tags: ['July 2026'], vibe_coded: true }, - { tags: ['not a date'], vibe_coded: true }, + { submission_date: '2026-07-10T10:00:00+00:00', vibe_coded: true }, + { submission_date: null, vibe_coded: true }, { vibe_coded: true }, ] const result = getSubmissionsTrendByVibeCoded(projects, referenceDate) diff --git a/src/utils/submissionsTrend.js b/src/utils/submissionsTrend.js index b3476ff..07b33fa 100644 --- a/src/utils/submissionsTrend.js +++ b/src/utils/submissionsTrend.js @@ -3,25 +3,23 @@ import { EXPERIENCE_LEVELS, getExperienceLevel } from './experience.js' // The two series names used by the vibe-coded trend chart. Exported so the // chart component can build its for each series without hardcoding // the strings itself (keeps the "source of truth" for series names here). -export const VIBE_CODED_SERIES = ['Not vibe-coded', 'Vibe-coded'] +export const VIBE_CODED_SERIES = ['Vibe-coded', 'Not vibe-coded'] const MONTH_NAMES = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ] -// Tag dates look like "July 2026" or "Date unspecified". -// Turns that raw string into a { year, monthIndex } object, or null if it -// doesn't match the expected "Month Year" shape (covers "Date unspecified", -// missing tags, typos, etc. — anything we can't confidently parse). -function parseTagMonth(rawDate) { - const match = /^([A-Za-z]+) (\d{4})$/.exec(rawDate ?? '') - if (!match) return null +// submission_date is an ISO timestamp like "2026-07-19T14:54:00+00:00", or +// null for projects that don't have one. Turns it into a { year, monthIndex } +// object in local time, or null when there's nothing to parse. +function parseSubmissionMonth(submissionDate) { + if (!submissionDate) return null - const monthIndex = MONTH_NAMES.indexOf(match[1]) - if (monthIndex === -1) return null + const date = new Date(submissionDate) + if (Number.isNaN(date.getTime())) return null - return { year: Number(match[2]), monthIndex } + return { year: date.getFullYear(), monthIndex: date.getMonth() } } // Turns { year, monthIndex } into a string like "2026-07" — used as a Map @@ -31,30 +29,38 @@ function monthKey(year, monthIndex) { return `${year}-${String(monthIndex + 1).padStart(2, '0')}` } -// Builds the 12 calendar months ending with referenceDate's month, oldest first. -function last12Months(referenceDate) { +// Builds the 12 calendar months ending with `end`, oldest first. +function last12Months(end) { const months = [] for (let i = 11; i >= 0; i--) { - const d = new Date(referenceDate.getFullYear(), referenceDate.getMonth() - i, 1) + const d = new Date(end.year, end.monthIndex - i, 1) months.push({ year: d.getFullYear(), monthIndex: d.getMonth() }) } return months } +// Returns the most recently *completed* calendar month before referenceDate, +// as a { year, monthIndex } pair — e.g. if referenceDate is any day in +// August, this returns July, since August itself isn't over yet and its +// submission count is still incomplete. +function mostRecentCompletedMonth(referenceDate) { + const d = new Date(referenceDate.getFullYear(), referenceDate.getMonth() - 1, 1) + return { year: d.getFullYear(), monthIndex: d.getMonth() } +} + // Builds the data for SubmissionsTrendChart: total submission count per -// month, for the trailing 12 months ending at referenceDate. +// month, for the trailing 12 months ending at the most recently completed +// month before referenceDate. export function getSubmissionsMonthlyTrend(projects, referenceDate = new Date()) { - const months = last12Months(referenceDate) + const months = last12Months(mostRecentCompletedMonth(referenceDate)) // Start every month at 0 so months with no submissions still show up in // the chart instead of being skipped entirely. const counts = new Map(months.map(({ year, monthIndex }) => [monthKey(year, monthIndex), 0])) for (const project of projects) { - const [rawDate] = project.tags ?? [] - - const parsed = parseTagMonth(rawDate) - if (!parsed) continue // unparseable/missing date — skip this project + const parsed = parseSubmissionMonth(project.submission_date) + if (!parsed) continue // no submission_date — skip this project const key = monthKey(parsed.year, parsed.monthIndex) if (counts.has(key)) { @@ -95,7 +101,7 @@ function monthsBetween(start, end) { export function getSubmissionsTrendByExperience(projects, referenceDate = new Date()) { const parsed = projects .map((project) => ({ - month: parseTagMonth(project.tags?.[0]), + month: parseSubmissionMonth(project.submission_date), level: getExperienceLevel(project.technical_experience), })) .filter((entry) => entry.month !== null) // drop projects with no usable date @@ -116,7 +122,7 @@ export function getSubmissionsTrendByExperience(projects, referenceDate = new Da return key < monthKey(earliestSoFar.year, earliestSoFar.monthIndex) ? entry.month : earliestSoFar }, withLevel[0].month) - const end = { year: referenceDate.getFullYear(), monthIndex: referenceDate.getMonth() } + const end = mostRecentCompletedMonth(referenceDate) const months = monthsBetween(earliest, end) // Each month starts with every experience level at 0, e.g. @@ -146,11 +152,12 @@ export function getSubmissionsTrendByExperience(projects, referenceDate = new Da // Builds the data for VibeCodedTrendChart: one row per month, with a // submission count for each of the 2 series ("Vibe-coded" / "Not -// vibe-coded") in that month. Always covers the trailing 12 months — -// unlike getSubmissionsTrendByExperience above, it doesn't hunt for an -// earliest-data month, so it can't return [] the way that one can. +// vibe-coded") in that month. Always covers the trailing 12 months ending +// at the most recently completed month — unlike getSubmissionsTrendByExperience +// above, it doesn't hunt for an earliest-data month, so it can't return [] +// the way that one can. export function getSubmissionsTrendByVibeCoded(projects, referenceDate = new Date()) { - const months = last12Months(referenceDate) + const months = last12Months(mostRecentCompletedMonth(referenceDate)) // Each month starts with both series at 0. const counts = new Map( @@ -161,9 +168,8 @@ export function getSubmissionsTrendByVibeCoded(projects, referenceDate = new Dat ) for (const project of projects) { - const [rawDate] = project.tags ?? [] - const parsed = parseTagMonth(rawDate) - if (!parsed) continue // unparseable/missing date — skip this project + const parsed = parseSubmissionMonth(project.submission_date) + if (!parsed) continue // no submission_date — skip this project const key = monthKey(parsed.year, parsed.monthIndex) if (!counts.has(key)) continue // outside the trailing-12-months window diff --git a/src/utils/techUsed.js b/src/utils/techUsed.js index 4b4333e..4906c1c 100644 --- a/src/utils/techUsed.js +++ b/src/utils/techUsed.js @@ -10,7 +10,7 @@ export const KNOWN_TECHNOLOGIES = [ { label: 'Next.js', patterns: ['next.js', 'nextjs'] }, { label: 'Vercel', patterns: ['vercel'] }, { label: 'Python', patterns: ['python'] }, - { label: 'MCP', patterns: ['mcp'] }, + { label: 'Sefaria MCP', patterns: ['mcp'] }, { label: 'Supabase', patterns: ['supabase'] }, { label: 'Deepgram', patterns: ['deepgram'] }, { label: '.NET', patterns: ['.net'] }, From 050c581acb30199ba780da490e45a58cd0b10d20 Mon Sep 17 00:00:00 2001 From: Sefaria Intern Date: Mon, 10 Aug 2026 13:06:17 +0300 Subject: [PATCH 2/3] Fetch projects only from the production Sefaria API The local dev API merge is no longer needed now that the production endpoint is up to date. Also fixes .gitignore: the unanchored data/ rule was accidentally shadowing src/data/ too; the anchored /data/ rule already covers the intended raw-dump exclusion. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 3 --- src/data/fetchProjects.js | 14 ++------------ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 9228279..4072425 100644 --- a/.gitignore +++ b/.gitignore @@ -15,9 +15,6 @@ dist-ssr docs/superpowers/ .worktrees/ -# Raw local data dumps (may contain sensitive info, never commit) -data/ - # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/src/data/fetchProjects.js b/src/data/fetchProjects.js index c13bbfa..dce3b82 100644 --- a/src/data/fetchProjects.js +++ b/src/data/fetchProjects.js @@ -1,6 +1,5 @@ import { getCategories } from '../utils/categories.js' -const LOCAL_API_URL = 'http://localhost:8000/api/powered-by' const PROD_API_URL = 'https://www.sefaria.org/api/powered-by' async function fetchProjectList(url) { @@ -15,18 +14,9 @@ async function fetchProjectList(url) { } export async function fetchProjects() { - const [localProjects, prodProjects] = await Promise.all([ - fetchProjectList(LOCAL_API_URL), - fetchProjectList(PROD_API_URL), - ]) + const projects = await fetchProjectList(PROD_API_URL) - // later entries win ties, so prod overwrites local on matching project_name - const byName = new Map() - for (const project of [...localProjects, ...prodProjects]) { - byName.set(project.project_name, project) - } - - return [...byName.values()] + return projects .filter((project) => project.is_published && project.consent_to_display) .map((project) => ({ ...project, From 17c4d6173cd0a1307b4d1df9180aeff1e54d206a Mon Sep 17 00:00:00 2001 From: Sefaria Intern Date: Tue, 11 Aug 2026 10:19:01 +0300 Subject: [PATCH 3/3] Fix design/code-review feedback: UTC month bucketing, wrapping subheader, safe legend sorter - submissionsTrend.js: bucket submission months by UTC instead of local time so results don't shift depending on the viewer's timezone - index.css: allow the subheader description to wrap instead of forcing nowrap, which caused horizontal overflow on narrow viewports - LineChart.jsx: replace itemSorter={null} with a function that sorts legend items by their declared series order, since Recharts expects itemSorter to be callable Co-Authored-By: Claude Sonnet 5 --- src/components/charts/types/LineChart.jsx | 14 +++++++++++--- src/index.css | 1 - src/utils/submissionsTrend.js | 10 ++++++---- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/charts/types/LineChart.jsx b/src/components/charts/types/LineChart.jsx index 986f2fc..02c9939 100644 --- a/src/components/charts/types/LineChart.jsx +++ b/src/components/charts/types/LineChart.jsx @@ -16,9 +16,17 @@ function LineChart({ data, series, title, description, height = 360 }) { {/* Legend's default itemSorter is 'value', which alphabetizes entries by name — overriding the order series/ below are declared in (e.g. - "Not vibe-coded" would sort before "Vibe-coded"). Disable it to keep - declaration order instead. */} - + "Not vibe-coded" would sort before "Vibe-coded"). itemSorter must be a + function (or a string key) — Recharts calls it unconditionally, so + passing null risks breaking rather than disabling the sort. Sort by + each item's index in `series` instead, to preserve declaration order; + unmatched items (shouldn't normally happen) go last. */} + { + const index = series.findIndex(({ name }) => name === item.value) + return index === -1 ? series.length : index + }} + /> {series.map(({ key, name, color }) => (