Skip to content

Commit 137ea8c

Browse files
committed
feat: show the date range the export covers
Surface the time span of the analysed activity (from/to dates + approx years) as a banner atop the report, so a 1-year export isn't mistaken for a lifetime one. Derived from the earliest/latest interaction timestamps.
1 parent 38a00d5 commit 137ea8c

9 files changed

Lines changed: 42 additions & 5 deletions

File tree

src/i18n/locales/ca.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Català
22
export const ca: Record<string, string> = {
3+
'coverage': 'Basat en la teva activitat de {from} a {to} (~{years} anys).',
34
'drop.prompt':
45
'Tria o deixa caure el teu export d’Instagram (.zip en format JSON). No surt mai del teu navegador.',
56
'drop.helpSummary': 'Com descarregar l’export correcte',

src/i18n/locales/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// English — source of truth. Every other locale mirrors these keys.
22
// Placeholders use {name}. Usernames are never translated.
33
export const en: Record<string, string> = {
4+
'coverage': 'Based on your activity from {from} to {to} (~{years} years).',
45
'drop.prompt':
56
'Pick or drop your Instagram data export (.zip in JSON format). It never leaves your browser.',
67
'drop.helpSummary': 'How to download the right export',

src/i18n/locales/es.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Español
22
export const es: Record<string, string> = {
3+
'coverage': 'Basado en tu actividad de {from} a {to} (~{years} años).',
34
'drop.prompt':
45
'Elige o suelta tu export de Instagram (.zip en formato JSON). Nunca sale de tu navegador.',
56
'drop.helpSummary': 'Cómo descargar el export correcto',

src/i18n/locales/eu.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Euskara
22
export const eu: Record<string, string> = {
3+
'coverage': '{from}etik {to}ra arteko zure jardueran oinarrituta (~{years} urte).',
34
'drop.prompt':
45
'Aukeratu edo jaregin zure Instagram datu-esportazioa (.zip JSON formatuan). Ez da inoiz zure nabigatzailetik irteten.',
56
'drop.helpSummary': 'Nola deskargatu esportazio egokia',

src/i18n/locales/gl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Galego
22
export const gl: Record<string, string> = {
3+
'coverage': 'Baseado na túa actividade de {from} a {to} (~{years} anos).',
34
'drop.prompt':
45
'Escolle ou solta o teu export de Instagram (.zip en formato JSON). Non sae nunca do teu navegador.',
56
'drop.helpSummary': 'Como descargar o export correcto',

src/i18n/locales/va.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Valencià
22
export const va: Record<string, string> = {
3+
'coverage': 'Basat en la teua activitat de {from} a {to} (~{years} anys).',
34
'drop.prompt':
45
'Tria o arrossega el teu export d\'Instagram (.zip en format JSON). Mai ix del teu navegador.',
56
'drop.helpSummary': 'Com descarregar l\'export correcte',

src/insights.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,14 @@ export interface Insights {
2020
/** Bias toward your own circle: share of attention spent on mutuals / on accounts you follow. */
2121
bubble: { mutualAttentionShare: number; followedAttentionShare: number }
2222
closeFriends: { total: number; engaged: number }
23-
temporal: { byHour: number[]; busiestHour: number; spanDays: number; dated: number }
23+
temporal: {
24+
byHour: number[]
25+
busiestHour: number
26+
spanDays: number
27+
dated: number
28+
firstTs: number // unix seconds of the earliest dated interaction
29+
lastTs: number // unix seconds of the latest dated interaction
30+
}
2431
}
2532

2633
const intersectSize = (a: Set<string>, b: Set<string>): number => {
@@ -70,8 +77,9 @@ export function computeInsights(data: NormalizedData): Insights {
7077
byHour[new Date(i.timestamp * 1000).getUTCHours()]++
7178
}
7279
const busiestHour = byHour.indexOf(Math.max(...byHour))
73-
const spanDays =
74-
stamps.length > 1 ? Math.round((Math.max(...stamps) - Math.min(...stamps)) / 86400) : 0
80+
const firstTs = stamps.length ? Math.min(...stamps) : 0
81+
const lastTs = stamps.length ? Math.max(...stamps) : 0
82+
const spanDays = stamps.length > 1 ? Math.round((lastTs - firstTs) / 86400) : 0
7583

7684
return {
7785
hasFollowerData,
@@ -95,6 +103,13 @@ export function computeInsights(data: NormalizedData): Insights {
95103
total: close.size,
96104
engaged: [...close].filter((a) => engaged.has(a)).length,
97105
},
98-
temporal: { byHour, busiestHour: busiestHour < 0 ? 0 : busiestHour, spanDays, dated: stamps.length },
106+
temporal: {
107+
byHour,
108+
busiestHour: busiestHour < 0 ? 0 : busiestHour,
109+
spanDays,
110+
dated: stamps.length,
111+
firstTs,
112+
lastTs,
113+
},
99114
}
100115
}

src/style.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
2424
th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid #eee; white-space: nowrap; }
2525

26+
.coverage { text-align: center; font-size: 0.82rem; color: #666; background: #f5f5f5; border-radius: 999px; padding: 0.35rem 0.8rem; margin: 0 0 0.8rem; }
2627
.gauge { margin: 0.4rem 0 0.2rem; }
2728
.gauge-score { font-size: 1rem; }
2829
.gauge-num { font-size: 2.4rem; font-weight: 800; }

src/ui/view.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { t } from '../i18n'
1+
import { getLocale, t } from '../i18n'
22
import type { Report } from '../report-model'
33
import { renderLorenz, renderTopBars } from './charts'
44
import { renderGraph } from './graph'
@@ -131,6 +131,21 @@ function renderInsightSections(root: HTMLElement, report: Report): void {
131131
export function renderReport(root: HTMLElement, report: Report): void {
132132
root.innerHTML = ''
133133

134+
// Coverage indicator: the export's date range drives everything below.
135+
const tmp = report.insights.temporal
136+
if (tmp.dated > 0) {
137+
const fmt = (ts: number) =>
138+
new Date(ts * 1000).toLocaleDateString(getLocale(), { year: 'numeric', month: 'short' })
139+
const banner = document.createElement('p')
140+
banner.className = 'coverage'
141+
banner.textContent = t('coverage', {
142+
from: fmt(tmp.firstTs),
143+
to: fmt(tmp.lastTs),
144+
years: (tmp.spanDays / 365).toFixed(1),
145+
})
146+
root.appendChild(banner)
147+
}
148+
134149
if (report.totalInteractions < MIN_RELIABLE_INTERACTIONS) {
135150
root.appendChild(notice(t('notice.lowData', { n: report.totalInteractions }), 'warn'))
136151
}

0 commit comments

Comments
 (0)