Add defensive null guards to all chart components and analytics
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m59s

All 9 chart components now have early-return null/empty checks before
calling .map() on data props. The diversity-metrics chart guards all
nested array fields (byCountry, byCategory, byOceanIssue, byTag).
Analytics backend guards p.tags in getDiversityMetrics. This prevents
any "Cannot read properties of null (reading 'map')" crashes even if
upstream data shapes are unexpected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-20 13:42:31 +01:00
parent 4519bc6080
commit fbcbf895be
10 changed files with 43 additions and 11 deletions

View File

@@ -23,6 +23,8 @@ type CriterionBarDatum = {
} }
export function CriteriaScoresChart({ data }: CriteriaScoresProps) { export function CriteriaScoresChart({ data }: CriteriaScoresProps) {
if (!data?.length) return null
const overallAverage = const overallAverage =
data.length > 0 data.length > 0
? data.reduce((sum, d) => sum + d.averageScore, 0) / data.length ? data.reduce((sum, d) => sum + d.averageScore, 0) / data.length

View File

@@ -21,6 +21,16 @@ interface CrossStageComparisonProps {
export function CrossStageComparisonChart({ export function CrossStageComparisonChart({
data, data,
}: CrossStageComparisonProps) { }: CrossStageComparisonProps) {
if (!data?.length) {
return (
<Card>
<CardContent className="flex items-center justify-center py-12">
<p className="text-muted-foreground">No comparison data available</p>
</CardContent>
</Card>
)
}
const baseData = data.map((round) => ({ const baseData = data.map((round) => ({
name: name:
round.roundName.length > 20 round.roundName.length > 20

View File

@@ -39,7 +39,7 @@ function formatLabel(value: string): string {
} }
export function DiversityMetricsChart({ data }: DiversityMetricsProps) { export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
if (data.total === 0) { if (!data || data.total === 0) {
return ( return (
<Card> <Card>
<CardContent className="flex items-center justify-center py-12"> <CardContent className="flex items-center justify-center py-12">
@@ -50,8 +50,8 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
} }
// Top countries for pie chart (max 10, others grouped) // Top countries for pie chart (max 10, others grouped)
const topCountries = data.byCountry.slice(0, 10) const topCountries = (data.byCountry || []).slice(0, 10)
const otherCountries = data.byCountry.slice(10) const otherCountries = (data.byCountry || []).slice(10)
const countryPieData = otherCountries.length > 0 const countryPieData = otherCountries.length > 0
? [...topCountries, { ? [...topCountries, {
country: 'Others', country: 'Others',
@@ -67,12 +67,12 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
})) }))
// Pre-format category and ocean issue data for display // Pre-format category and ocean issue data for display
const formattedCategories = data.byCategory.slice(0, 10).map((c) => ({ const formattedCategories = (data.byCategory || []).slice(0, 10).map((c) => ({
category: formatLabel(c.category), category: formatLabel(c.category),
count: c.count, count: c.count,
})) }))
const formattedOceanIssues = data.byOceanIssue.slice(0, 15).map((o) => ({ const formattedOceanIssues = (data.byOceanIssue || []).slice(0, 15).map((o) => ({
issue: formatLabel(o.issue), issue: formatLabel(o.issue),
count: o.count, count: o.count,
})) }))
@@ -89,19 +89,19 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
</Card> </Card>
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="text-2xl font-bold">{data.byCountry.length}</div> <div className="text-2xl font-bold">{(data.byCountry || []).length}</div>
<p className="text-sm text-muted-foreground">Countries Represented</p> <p className="text-sm text-muted-foreground">Countries Represented</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="text-2xl font-bold">{data.byCategory.length}</div> <div className="text-2xl font-bold">{(data.byCategory || []).length}</div>
<p className="text-sm text-muted-foreground">Categories</p> <p className="text-sm text-muted-foreground">Categories</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="text-2xl font-bold">{data.byTag.length}</div> <div className="text-2xl font-bold">{(data.byTag || []).length}</div>
<p className="text-sm text-muted-foreground">Unique Tags</p> <p className="text-sm text-muted-foreground">Unique Tags</p>
</CardContent> </CardContent>
</Card> </Card>
@@ -228,14 +228,14 @@ export function DiversityMetricsChart({ data }: DiversityMetricsProps) {
)} )}
{/* Tags Cloud */} {/* Tags Cloud */}
{data.byTag.length > 0 && ( {(data.byTag || []).length > 0 && (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Project Tags</CardTitle> <CardTitle>Project Tags</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{data.byTag.slice(0, 30).map((tag) => ( {(data.byTag || []).slice(0, 30).map((tag) => (
<Badge <Badge
key={tag.tag} key={tag.tag}
variant="secondary" variant="secondary"

View File

@@ -15,6 +15,8 @@ interface EvaluationTimelineProps {
} }
export function EvaluationTimelineChart({ data }: EvaluationTimelineProps) { export function EvaluationTimelineChart({ data }: EvaluationTimelineProps) {
if (!data?.length) return null
const formattedData = data.map((d) => ({ const formattedData = data.map((d) => ({
...d, ...d,
dateFormatted: new Date(d.date).toLocaleDateString('en-US', { dateFormatted: new Date(d.date).toLocaleDateString('en-US', {

View File

@@ -91,6 +91,16 @@ function CustomNode({
} }
export function JurorConsistencyChart({ data }: JurorConsistencyProps) { export function JurorConsistencyChart({ data }: JurorConsistencyProps) {
if (!data?.jurors?.length) {
return (
<Card>
<CardContent className="flex items-center justify-center py-12">
<p className="text-muted-foreground">No juror consistency data available</p>
</CardContent>
</Card>
)
}
const scatterData = [ const scatterData = [
{ {
id: 'Jurors', id: 'Jurors',

View File

@@ -25,6 +25,8 @@ type WorkloadBarDatum = {
} }
export function JurorWorkloadChart({ data }: JurorWorkloadProps) { export function JurorWorkloadChart({ data }: JurorWorkloadProps) {
if (!data?.length) return null
const totalAssigned = data.reduce((sum, d) => sum + d.assigned, 0) const totalAssigned = data.reduce((sum, d) => sum + d.assigned, 0)
const totalCompleted = data.reduce((sum, d) => sum + d.completed, 0) const totalCompleted = data.reduce((sum, d) => sum + d.completed, 0)
const overallRate = const overallRate =

View File

@@ -30,6 +30,8 @@ export function ProjectRankingsChart({
data, data,
limit = 20, limit = 20,
}: ProjectRankingsProps) { }: ProjectRankingsProps) {
if (!data?.length) return null
const scoredData = data.filter( const scoredData = data.filter(
(d): d is ProjectRankingData & { averageScore: number } => (d): d is ProjectRankingData & { averageScore: number } =>
d.averageScore !== null, d.averageScore !== null,

View File

@@ -15,6 +15,8 @@ export function ScoreDistributionChart({
averageScore, averageScore,
totalScores, totalScores,
}: ScoreDistributionProps) { }: ScoreDistributionProps) {
if (!data?.length) return null
const chartData = data.map((d) => ({ const chartData = data.map((d) => ({
score: String(d.score), score: String(d.score),
count: d.count, count: d.count,

View File

@@ -14,6 +14,8 @@ interface StatusBreakdownProps {
} }
export function StatusBreakdownChart({ data }: StatusBreakdownProps) { export function StatusBreakdownChart({ data }: StatusBreakdownProps) {
if (!data?.length) return null
const total = data.reduce((sum, item) => sum + item.count, 0) const total = data.reduce((sum, item) => sum + item.count, 0)
const pieData = data.map((d) => ({ const pieData = data.map((d) => ({

View File

@@ -615,7 +615,7 @@ export const analyticsRouter = router({
// By tag // By tag
const tagCounts: Record<string, number> = {} const tagCounts: Record<string, number> = {}
projects.forEach((p) => { projects.forEach((p) => {
p.tags.forEach((tag) => { (p.tags || []).forEach((tag) => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1 tagCounts[tag] = (tagCounts[tag] || 0) + 1
}) })
}) })