Skip to content

Commit b3efcf7

Browse files
committed
add percentile stats!
1 parent 94c1fa3 commit b3efcf7

File tree

13 files changed

+125
-29
lines changed

13 files changed

+125
-29
lines changed

web/app/[year]/[id]/page.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default function WrappedPage() {
2121
const params = useParams();
2222
const [data, setData] = useState(null);
2323
const [percentiles, setPercentiles] = useState({});
24+
const [totalWraps, setTotalWraps] = useState(0);
2425
const [loading, setLoading] = useState(true);
2526
const [error, setError] = useState(null);
2627

@@ -48,6 +49,7 @@ export default function WrappedPage() {
4849
if (percentileResponse.ok) {
4950
const percentileData = await percentileResponse.json();
5051
setPercentiles(percentileData.percentiles || {});
52+
setTotalWraps(percentileData.total || 0);
5153
}
5254
} catch (err) {
5355
setError(err.message);
@@ -79,19 +81,19 @@ export default function WrappedPage() {
7981

8082
return (
8183
<main className="container">
82-
<HeroSection year={data.year} volume={stats.volume} percentiles={percentiles} />
84+
<HeroSection year={data.year} volume={stats.volume} percentiles={percentiles} totalWraps={totalWraps} />
8385
<HeatmapSection volume={stats.volume} year={data.year} />
8486
<TemporalSection temporal={stats.temporal} />
85-
<ContactsSection contacts={stats.contacts} percentiles={percentiles} />
86-
<ContentSection content={stats.content} percentiles={percentiles} />
87+
<ContactsSection contacts={stats.contacts} percentiles={percentiles} totalWraps={totalWraps} />
88+
<ContentSection content={stats.content} percentiles={percentiles} totalWraps={totalWraps} />
8789
<MessageAnalysisSection sentiment={stats.content?.sentiment} />
88-
<MessageLengthSection content={stats.content} percentiles={percentiles} />
89-
<ConversationsSection conversations={stats.conversations} percentiles={percentiles} />
90-
<GhostSection ghosts={stats.ghosts} percentiles={percentiles} />
90+
<MessageLengthSection content={stats.content} percentiles={percentiles} totalWraps={totalWraps} />
91+
<ConversationsSection conversations={stats.conversations} percentiles={percentiles} totalWraps={totalWraps} />
92+
<GhostSection ghosts={stats.ghosts} percentiles={percentiles} totalWraps={totalWraps} />
9193
{/* <CliffhangerSection cliffhangers={stats.cliffhangers} /> */}
92-
<ResponseTimesSection response_times={stats.response_times} percentiles={percentiles} />
93-
<TapbacksSection tapbacks={stats.tapbacks} percentiles={percentiles} />
94-
<StreaksSection streaks={stats.streaks} percentiles={percentiles} />
94+
<ResponseTimesSection response_times={stats.response_times} percentiles={percentiles} totalWraps={totalWraps} />
95+
<TapbacksSection tapbacks={stats.tapbacks} percentiles={percentiles} totalWraps={totalWraps} />
96+
<StreaksSection streaks={stats.streaks} percentiles={percentiles} totalWraps={totalWraps} />
9597
<WrappedFooter views={data.views} volume={stats.volume} />
9698
</main>
9799
);

web/app/api/percentiles/[year]/[id]/route.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ function calculatePercentile(value, allValues) {
1717

1818
// Helper to extract stat from data JSON
1919
function extractStat(data, path) {
20+
// Handle both data.raw.X and data.X structures
21+
let value = data?.raw || data;
22+
2023
const keys = path.split(".");
21-
let value = data;
2224
for (const key of keys) {
2325
if (value === null || value === undefined) return null;
2426
value = value[key];
@@ -63,6 +65,7 @@ export async function GET(request, { params }) {
6365
"volume.total_received",
6466
"content.avg_message_length_sent",
6567
"content.avg_message_length_received",
68+
"content.avg_word_count_sent",
6669
"content.questions_percentage",
6770
"content.enthusiasm_percentage",
6871
"content.attachments_sent",
@@ -72,11 +75,16 @@ export async function GET(request, { params }) {
7275
"conversations.one_on_one_chats",
7376
"content.double_text_count",
7477
"response_times.avg_response_time_minutes",
78+
"response_times.median_response_time_you_seconds",
79+
"response_times.median_response_time_them_seconds",
7580
"tapbacks.total_tapbacks_given",
7681
"tapbacks.total_tapbacks_received",
7782
"streaks.longest_streak_days",
7883
"contacts.unique_contacts_messaged",
7984
"contacts.unique_contacts_received_from",
85+
"ghosts.people_you_left_hanging",
86+
"ghosts.people_who_left_you_hanging",
87+
"ghosts.ghost_ratio",
8088
];
8189

8290
// Extract all values for each stat
@@ -106,7 +114,10 @@ export async function GET(request, { params }) {
106114
}
107115
});
108116

109-
return NextResponse.json({ percentiles });
117+
return NextResponse.json({
118+
percentiles,
119+
total: result.rows.length
120+
});
110121
} catch (error) {
111122
console.error("Percentile calculation error:", error);
112123
return NextResponse.json(

web/app/globals.css

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,17 +62,53 @@ body {
6262

6363
.percentile-badge {
6464
position: absolute;
65-
top: 0.75rem;
66-
right: 0.75rem;
65+
top: 0.5rem;
66+
right: 0.5rem;
6767
background: rgba(139, 92, 246, 0.2);
6868
border: 1px solid rgba(139, 92, 246, 0.4);
69-
border-radius: 0.5rem;
70-
padding: 0.25rem 0.5rem;
71-
font-size: 0.75rem;
69+
border-radius: 0.375rem;
70+
padding: 0.15rem 0.35rem;
71+
font-size: 0.65rem;
7272
font-weight: 600;
7373
color: rgba(139, 92, 246, 1);
7474
text-transform: uppercase;
7575
letter-spacing: 0.05em;
76+
cursor: help;
77+
transition: all 0.2s ease;
78+
}
79+
80+
.percentile-badge:hover {
81+
background: rgba(139, 92, 246, 0.3);
82+
border-color: rgba(139, 92, 246, 0.6);
83+
transform: scale(1.05);
84+
}
85+
86+
.percentile-tooltip {
87+
visibility: hidden;
88+
opacity: 0;
89+
position: absolute;
90+
top: 100%;
91+
right: 0;
92+
margin-top: 0.5rem;
93+
background: rgba(30, 30, 40, 0.98);
94+
color: white;
95+
padding: 0.5rem 0.75rem;
96+
border-radius: 0.5rem;
97+
font-size: 0.75rem;
98+
font-weight: 500;
99+
white-space: nowrap;
100+
z-index: 1000;
101+
pointer-events: none;
102+
border: 1px solid rgba(139, 92, 246, 0.3);
103+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
104+
text-transform: none;
105+
letter-spacing: normal;
106+
transition: opacity 0.2s ease, visibility 0.2s ease;
107+
}
108+
109+
.percentile-badge:hover .percentile-tooltip {
110+
visibility: visible;
111+
opacity: 1;
76112
}
77113

78114
.stat-label {

web/components/ContactsSection.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import StatCard from "./StatCard";
22
import ContactDistributionChart from "./ContactDistributionChart";
33

4-
export default function ContactsSection({ contacts, percentiles = {} }) {
4+
export default function ContactsSection({ contacts, percentiles = {}, totalWraps = 0 }) {
55
if (!contacts) return null;
66

77
return (
@@ -52,6 +52,7 @@ export default function ContactsSection({ contacts, percentiles = {} }) {
5252
label="Unique Contacts Messaged"
5353
value={contacts.unique_contacts_messaged}
5454
percentile={percentiles["contacts.unique_contacts_messaged"]}
55+
totalWraps={totalWraps}
5556
valueStyle={{ fontSize: "2rem" }}
5657
/>
5758
)}
@@ -60,6 +61,7 @@ export default function ContactsSection({ contacts, percentiles = {} }) {
6061
label="Unique Contacts Received From"
6162
value={contacts.unique_contacts_received_from}
6263
percentile={percentiles["contacts.unique_contacts_received_from"]}
64+
totalWraps={totalWraps}
6365
valueStyle={{ fontSize: "2rem" }}
6466
/>
6567
)}

web/components/ContentSection.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import PhraseHighlights from "./PhraseHighlights";
33
import EnhancedText from "./EnhancedText";
44
import { useEnhancement, PLAYFUL_INSTRUCTION } from "@/hooks/useEnhancement";
55

6-
export default function ContentSection({ content, percentiles = {} }) {
6+
export default function ContentSection({ content, percentiles = {}, totalWraps = 0 }) {
77
if (!content) return null;
88

99
return (
@@ -16,6 +16,7 @@ export default function ContentSection({ content, percentiles = {} }) {
1616
label="Avg Message Length (Sent)"
1717
value={`${Math.round(content.avg_message_length_sent)} chars`}
1818
percentile={percentiles["content.avg_message_length_sent"]}
19+
totalWraps={totalWraps}
1920
valueStyle={{ fontSize: "2rem" }}
2021
/>
2122
)}
@@ -24,6 +25,7 @@ export default function ContentSection({ content, percentiles = {} }) {
2425
label="Avg Message Length (Received)"
2526
value={`${Math.round(content.avg_message_length_received)} chars`}
2627
percentile={percentiles["content.avg_message_length_received"]}
28+
totalWraps={totalWraps}
2729
valueStyle={{ fontSize: "2rem" }}
2830
/>
2931
)}
@@ -32,6 +34,7 @@ export default function ContentSection({ content, percentiles = {} }) {
3234
label="❓ Questions Asked"
3335
value={`${content.questions_percentage}%`}
3436
percentile={percentiles["content.questions_percentage"]}
37+
totalWraps={totalWraps}
3538
valueStyle={{ fontSize: "2rem" }}
3639
/>
3740
)}
@@ -40,6 +43,7 @@ export default function ContentSection({ content, percentiles = {} }) {
4043
label="❗ Enthusiasm Level"
4144
value={`${content.enthusiasm_percentage}%`}
4245
percentile={percentiles["content.enthusiasm_percentage"]}
46+
totalWraps={totalWraps}
4347
valueStyle={{ fontSize: "2rem" }}
4448
/>
4549
)}
@@ -48,6 +52,7 @@ export default function ContentSection({ content, percentiles = {} }) {
4852
label="📎 Attachments Sent"
4953
value={content.attachments_sent.toLocaleString()}
5054
percentile={percentiles["content.attachments_sent"]}
55+
totalWraps={totalWraps}
5156
valueStyle={{ fontSize: "2rem" }}
5257
/>
5358
)}
@@ -56,12 +61,13 @@ export default function ContentSection({ content, percentiles = {} }) {
5661
label="📎 Attachments Received"
5762
value={content.attachments_received.toLocaleString()}
5863
percentile={percentiles["content.attachments_received"]}
64+
totalWraps={totalWraps}
5965
valueStyle={{ fontSize: "2rem" }}
6066
/>
6167
)}
6268
</div>
6369

64-
<DoubleTextSection content={content} percentiles={percentiles} />
70+
<DoubleTextSection content={content} percentiles={percentiles} totalWraps={totalWraps} />
6571

6672
<EmojiSection content={content} />
6773

@@ -73,7 +79,7 @@ export default function ContentSection({ content, percentiles = {} }) {
7379
);
7480
}
7581

76-
function DoubleTextSection({ content, percentiles }) {
82+
function DoubleTextSection({ content, percentiles, totalWraps }) {
7783
const prompt =
7884
content.double_text_count !== undefined
7985
? `You sent ${content.double_text_count} double texts, that's ${content.double_text_percentage}% of your messages. ${PLAYFUL_INSTRUCTION}`
@@ -109,6 +115,7 @@ function DoubleTextSection({ content, percentiles }) {
109115
label="Double Texts Sent"
110116
value={content.double_text_count.toLocaleString()}
111117
percentile={percentiles["content.double_text_count"]}
118+
totalWraps={totalWraps}
112119
valueStyle={{ fontSize: "2.5rem", color: "#06b6d4" }}
113120
/>
114121
</div>

web/components/ConversationsSection.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import StatCard from "./StatCard";
22

3-
export default function ConversationsSection({ conversations, percentiles = {} }) {
3+
export default function ConversationsSection({ conversations, percentiles = {}, totalWraps = 0 }) {
44
if (!conversations) return null;
55

66
return (
@@ -12,6 +12,7 @@ export default function ConversationsSection({ conversations, percentiles = {} }
1212
label="Total Conversations"
1313
value={conversations.total_conversations}
1414
percentile={percentiles["conversations.total_conversations"]}
15+
totalWraps={totalWraps}
1516
valueStyle={{ fontSize: "2rem" }}
1617
/>
1718
)}
@@ -20,6 +21,7 @@ export default function ConversationsSection({ conversations, percentiles = {} }
2021
label="Group Chats"
2122
value={conversations.group_chats}
2223
percentile={percentiles["conversations.group_chats"]}
24+
totalWraps={totalWraps}
2325
valueStyle={{ fontSize: "2rem" }}
2426
/>
2527
)}
@@ -28,6 +30,7 @@ export default function ConversationsSection({ conversations, percentiles = {} }
2830
label="1-on-1 Chats"
2931
value={conversations.one_on_one_chats}
3032
percentile={percentiles["conversations.one_on_one_chats"]}
33+
totalWraps={totalWraps}
3134
valueStyle={{ fontSize: "2rem" }}
3235
/>
3336
)}

web/components/GhostSection.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import StatCard from "./StatCard";
22

3-
export default function GhostSection({ ghosts, percentiles = {} }) {
3+
export default function GhostSection({ ghosts, percentiles = {}, totalWraps = 0 }) {
44
if (!ghosts) return null;
55

66
const totalYou = ghosts.people_you_left_hanging || 0;
@@ -25,17 +25,23 @@ export default function GhostSection({ ghosts, percentiles = {} }) {
2525
<StatCard
2626
label="People You Left Hanging"
2727
value={totalYou.toLocaleString()}
28+
percentile={percentiles["ghosts.people_you_left_hanging"]}
29+
totalWraps={totalWraps}
2830
valueStyle={{ fontSize: "2rem" }}
2931
/>
3032
<StatCard
3133
label="People Who Left You Hanging"
3234
value={totalThem.toLocaleString()}
35+
percentile={percentiles["ghosts.people_who_left_you_hanging"]}
36+
totalWraps={totalWraps}
3337
valueStyle={{ fontSize: "2rem" }}
3438
/>
3539
{typeof ratio === "number" && (
3640
<StatCard
3741
label="Ghost Ratio (You/Them)"
3842
value={ratio.toFixed(2)}
43+
percentile={percentiles["ghosts.ghost_ratio"]}
44+
totalWraps={totalWraps}
3945
valueStyle={{ fontSize: "2rem" }}
4046
/>
4147
)}

web/components/HeroSection.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import StatCard from "./StatCard";
22

3-
export default function HeroSection({ year, volume, percentiles = {} }) {
3+
export default function HeroSection({ year, volume, percentiles = {}, totalWraps = 0 }) {
44
return (
55
<div className="hero">
66
<h1>
@@ -12,12 +12,14 @@ export default function HeroSection({ year, volume, percentiles = {} }) {
1212
label="Messages Sent"
1313
value={volume?.total_sent?.toLocaleString() || 0}
1414
percentile={percentiles["volume.total_sent"]}
15+
totalWraps={totalWraps}
1516
/>
1617

1718
<StatCard
1819
label="Messages Received"
1920
value={volume?.total_received?.toLocaleString() || 0}
2021
percentile={percentiles["volume.total_received"]}
22+
totalWraps={totalWraps}
2123
valueStyle={{
2224
background: "linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%)",
2325
WebkitBackgroundClip: "text",

web/components/MessageLengthSection.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import StatCard from "./StatCard";
22
import { useEnhancement, PLAYFUL_INSTRUCTION } from "@/hooks/useEnhancement";
33
import { Histogram, getWarmColorGradient } from "@/lib/histogram";
44

5-
export default function MessageLengthSection({ content, percentiles = {} }) {
5+
export default function MessageLengthSection({ content, percentiles = {}, totalWraps = 0 }) {
66
if (!content) return null;
77

88
const hasLengthData =
@@ -21,6 +21,7 @@ export default function MessageLengthSection({ content, percentiles = {} }) {
2121
label="Avg Length (Sent)"
2222
value={`${Math.round(content.avg_message_length_sent)} chars`}
2323
percentile={percentiles["content.avg_message_length_sent"]}
24+
totalWraps={totalWraps}
2425
valueStyle={{ fontSize: "2rem" }}
2526
/>
2627
)}
@@ -29,13 +30,16 @@ export default function MessageLengthSection({ content, percentiles = {} }) {
2930
label="Avg Length (Received)"
3031
value={`${Math.round(content.avg_message_length_received)} chars`}
3132
percentile={percentiles["content.avg_message_length_received"]}
33+
totalWraps={totalWraps}
3234
valueStyle={{ fontSize: "2rem" }}
3335
/>
3436
)}
3537
{content.avg_word_count_sent !== undefined && (
3638
<StatCard
3739
label="Avg Words (Sent)"
3840
value={`${Math.round(content.avg_word_count_sent)} words`}
41+
percentile={percentiles["content.avg_word_count_sent"]}
42+
totalWraps={totalWraps}
3943
valueStyle={{ fontSize: "2rem" }}
4044
/>
4145
)}

0 commit comments

Comments
 (0)