Skip to content

Commit 01410ac

Browse files
committed
web stuff
1 parent e86e7d5 commit 01410ac

File tree

15 files changed

+170
-34
lines changed

15 files changed

+170
-34
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "imessage-wrapped"
7-
version = "0.1.19"
7+
version = "0.1.20"
88
description = "Export and analyze iMessage conversations from macOS SQLite database"
99
readme = "README.md"
1010
requires-python = ">=3.10"

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ export default function WrappedPage() {
2121
const params = useParams();
2222
const [data, setData] = useState(null);
2323
const [percentiles, setPercentiles] = useState({});
24+
const [ranks, setRanks] = useState({});
25+
const [metricCounts, setMetricCounts] = useState({});
2426
const [totalWraps, setTotalWraps] = useState(0);
2527
const [loading, setLoading] = useState(true);
2628
const [error, setError] = useState(null);
@@ -49,6 +51,8 @@ export default function WrappedPage() {
4951
if (percentileResponse.ok) {
5052
const percentileData = await percentileResponse.json();
5153
setPercentiles(percentileData.percentiles || {});
54+
setRanks(percentileData.ranks || {});
55+
setMetricCounts(percentileData.metricCounts || {});
5256
setTotalWraps(percentileData.total || 0);
5357
}
5458
} catch (err) {
@@ -82,20 +86,20 @@ export default function WrappedPage() {
8286

8387
return (
8488
<main className="container">
85-
<HeroSection year={data.year} volume={stats.volume} percentiles={percentiles} totalWraps={totalWraps} userName={userName} />
89+
<HeroSection year={data.year} volume={stats.volume} percentiles={percentiles} ranks={ranks} metricCounts={metricCounts} totalWraps={totalWraps} userName={userName} />
8690
<HeatmapSection volume={stats.volume} year={data.year} />
8791
<TemporalSection temporal={stats.temporal} />
88-
<ContactsSection contacts={stats.contacts} percentiles={percentiles} totalWraps={totalWraps} />
89-
<ContentSection content={stats.content} percentiles={percentiles} totalWraps={totalWraps} />
92+
<ContactsSection contacts={stats.contacts} percentiles={percentiles} ranks={ranks} metricCounts={metricCounts} totalWraps={totalWraps} />
93+
<ContentSection content={stats.content} percentiles={percentiles} ranks={ranks} metricCounts={metricCounts} totalWraps={totalWraps} />
9094
<MessageAnalysisSection sentiment={stats.content?.sentiment} />
91-
<MessageLengthSection content={stats.content} percentiles={percentiles} totalWraps={totalWraps} />
92-
<ConversationsSection conversations={stats.conversations} percentiles={percentiles} totalWraps={totalWraps} />
93-
<GhostSection ghosts={stats.ghosts} percentiles={percentiles} totalWraps={totalWraps} />
95+
<MessageLengthSection content={stats.content} percentiles={percentiles} ranks={ranks} metricCounts={metricCounts} totalWraps={totalWraps} />
96+
<ConversationsSection conversations={stats.conversations} percentiles={percentiles} ranks={ranks} metricCounts={metricCounts} totalWraps={totalWraps} />
97+
<GhostSection ghosts={stats.ghosts} percentiles={percentiles} ranks={ranks} metricCounts={metricCounts} totalWraps={totalWraps} />
9498
{/* <CliffhangerSection cliffhangers={stats.cliffhangers} /> */}
95-
<ResponseTimesSection response_times={stats.response_times} percentiles={percentiles} totalWraps={totalWraps} />
96-
<TapbacksSection tapbacks={stats.tapbacks} percentiles={percentiles} totalWraps={totalWraps} />
97-
<StreaksSection streaks={stats.streaks} percentiles={percentiles} totalWraps={totalWraps} />
98-
<WrappedFooter views={data.views} volume={stats.volume} percentiles={percentiles} totalWraps={totalWraps} />
99+
<ResponseTimesSection response_times={stats.response_times} percentiles={percentiles} ranks={ranks} metricCounts={metricCounts} totalWraps={totalWraps} />
100+
<TapbacksSection tapbacks={stats.tapbacks} percentiles={percentiles} ranks={ranks} metricCounts={metricCounts} totalWraps={totalWraps} />
101+
<StreaksSection streaks={stats.streaks} percentiles={percentiles} ranks={ranks} metricCounts={metricCounts} totalWraps={totalWraps} />
102+
<WrappedFooter views={data.views} volume={stats.volume} percentiles={percentiles} ranks={ranks} metricCounts={metricCounts} totalWraps={totalWraps} />
99103
</main>
100104
);
101105
}

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,21 +116,42 @@ export async function GET(request, { params }) {
116116
});
117117
});
118118

119-
// Calculate percentiles for current user
119+
// Calculate percentiles and ranks for current user
120120
const percentiles = {};
121+
const ranks = {};
122+
const metricCounts = {};
123+
121124
statsToTrack.forEach((stat) => {
122125
const currentValue = extractStat(currentWrap.data, stat);
123-
if (currentValue !== null && statValues[stat].length > 1) {
126+
const allValuesForStat = statValues[stat];
127+
128+
// Store the actual count of non-null values for this metric
129+
metricCounts[stat] = allValuesForStat.length;
130+
131+
if (currentValue !== null && allValuesForStat.length > 1) {
124132
percentiles[stat] = calculatePercentile(
125133
currentValue,
126-
statValues[stat],
134+
allValuesForStat,
127135
lowerIsBetterStats.has(stat)
128136
);
137+
138+
// Calculate actual rank (1-based)
139+
const sortedValues = [...allValuesForStat].sort((a, b) => {
140+
// For lower is better stats, sort ascending (smallest = rank 1)
141+
// For higher is better stats, sort descending (largest = rank 1)
142+
return lowerIsBetterStats.has(stat) ? a - b : b - a;
143+
});
144+
145+
// Find rank - handle ties by giving them the same rank
146+
const rank = sortedValues.findIndex(v => v === currentValue) + 1;
147+
ranks[stat] = rank;
129148
}
130149
});
131150

132151
return NextResponse.json({
133152
percentiles,
153+
ranks,
154+
metricCounts,
134155
total: result.rows.length
135156
});
136157
} catch (error) {

web/app/globals.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,28 @@ body {
8383
transform: scale(1.05);
8484
}
8585

86+
.percentile-badge-top10 {
87+
background: rgba(239, 68, 68, 0.2) !important;
88+
border-color: rgba(239, 68, 68, 0.4) !important;
89+
color: #fca5a5;
90+
}
91+
92+
.percentile-badge-top10:hover {
93+
background: rgba(239, 68, 68, 0.3) !important;
94+
border-color: rgba(239, 68, 68, 0.6) !important;
95+
}
96+
97+
.percentile-badge-first {
98+
background: rgba(234, 179, 8, 0.2) !important;
99+
border-color: rgba(234, 179, 8, 0.4) !important;
100+
color: #fde047;
101+
}
102+
103+
.percentile-badge-first:hover {
104+
background: rgba(234, 179, 8, 0.3) !important;
105+
border-color: rgba(234, 179, 8, 0.6) !important;
106+
}
107+
86108
.percentile-tooltip {
87109
visibility: hidden;
88110
opacity: 0;

web/components/ContactsSection.js

Lines changed: 5 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 = {}, totalWraps = 0 }) {
4+
export default function ContactsSection({ contacts, percentiles = {}, ranks = {}, metricCounts = {}, totalWraps = 0 }) {
55
if (!contacts) return null;
66

77
return (
@@ -52,6 +52,8 @@ export default function ContactsSection({ contacts, percentiles = {}, totalWraps
5252
label="Unique Contacts Messaged"
5353
value={contacts.unique_contacts_messaged}
5454
percentile={percentiles["contacts.unique_contacts_messaged"]}
55+
rank={ranks["contacts.unique_contacts_messaged"]}
56+
metricTotal={metricCounts["contacts.unique_contacts_messaged"]}
5557
totalWraps={totalWraps}
5658
valueStyle={{ fontSize: "2rem" }}
5759
/>
@@ -61,6 +63,8 @@ export default function ContactsSection({ contacts, percentiles = {}, totalWraps
6163
label="Unique Contacts Received From"
6264
value={contacts.unique_contacts_received_from}
6365
percentile={percentiles["contacts.unique_contacts_received_from"]}
66+
rank={ranks["contacts.unique_contacts_received_from"]}
67+
metricTotal={metricCounts["contacts.unique_contacts_received_from"]}
6468
totalWraps={totalWraps}
6569
valueStyle={{ fontSize: "2rem" }}
6670
/>

web/components/ContentSection.js

Lines changed: 19 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 = {}, totalWraps = 0 }) {
6+
export default function ContentSection({ content, percentiles = {}, ranks = {}, metricCounts = {}, totalWraps = 0 }) {
77
if (!content) return null;
88

99
return (
@@ -16,6 +16,8 @@ export default function ContentSection({ content, percentiles = {}, totalWraps =
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+
rank={ranks["content.avg_message_length_sent"]}
20+
metricTotal={metricCounts["content.avg_message_length_sent"]}
1921
totalWraps={totalWraps}
2022
valueStyle={{ fontSize: "2rem" }}
2123
/>
@@ -25,6 +27,8 @@ export default function ContentSection({ content, percentiles = {}, totalWraps =
2527
label="Avg Message Length (Received)"
2628
value={`${Math.round(content.avg_message_length_received)} chars`}
2729
percentile={percentiles["content.avg_message_length_received"]}
30+
rank={ranks["content.avg_message_length_received"]}
31+
metricTotal={metricCounts["content.avg_message_length_received"]}
2832
totalWraps={totalWraps}
2933
valueStyle={{ fontSize: "2rem" }}
3034
/>
@@ -34,6 +38,8 @@ export default function ContentSection({ content, percentiles = {}, totalWraps =
3438
label="❓ Questions Asked"
3539
value={`${content.questions_percentage}%`}
3640
percentile={percentiles["content.questions_percentage"]}
41+
rank={ranks["content.questions_percentage"]}
42+
metricTotal={metricCounts["content.questions_percentage"]}
3743
totalWraps={totalWraps}
3844
valueStyle={{ fontSize: "2rem" }}
3945
/>
@@ -43,6 +49,8 @@ export default function ContentSection({ content, percentiles = {}, totalWraps =
4349
label="❗ Enthusiasm Level"
4450
value={`${content.enthusiasm_percentage}%`}
4551
percentile={percentiles["content.enthusiasm_percentage"]}
52+
rank={ranks["content.enthusiasm_percentage"]}
53+
metricTotal={metricCounts["content.enthusiasm_percentage"]}
4654
totalWraps={totalWraps}
4755
valueStyle={{ fontSize: "2rem" }}
4856
/>
@@ -52,6 +60,8 @@ export default function ContentSection({ content, percentiles = {}, totalWraps =
5260
label="📎 Attachments Sent"
5361
value={content.attachments_sent.toLocaleString()}
5462
percentile={percentiles["content.attachments_sent"]}
63+
rank={ranks["content.attachments_sent"]}
64+
metricTotal={metricCounts["content.attachments_sent"]}
5565
totalWraps={totalWraps}
5666
valueStyle={{ fontSize: "2rem" }}
5767
/>
@@ -61,13 +71,15 @@ export default function ContentSection({ content, percentiles = {}, totalWraps =
6171
label="📎 Attachments Received"
6272
value={content.attachments_received.toLocaleString()}
6373
percentile={percentiles["content.attachments_received"]}
74+
rank={ranks["content.attachments_received"]}
75+
metricTotal={metricCounts["content.attachments_received"]}
6476
totalWraps={totalWraps}
6577
valueStyle={{ fontSize: "2rem" }}
6678
/>
6779
)}
6880
</div>
6981

70-
<DoubleTextSection content={content} percentiles={percentiles} totalWraps={totalWraps} />
82+
<DoubleTextSection content={content} percentiles={percentiles} ranks={ranks} metricCounts={metricCounts} totalWraps={totalWraps} />
7183

7284
<EmojiSection content={content} percentiles={percentiles} totalWraps={totalWraps} />
7385

@@ -79,7 +91,7 @@ export default function ContentSection({ content, percentiles = {}, totalWraps =
7991
);
8092
}
8193

82-
function DoubleTextSection({ content, percentiles, totalWraps }) {
94+
function DoubleTextSection({ content, percentiles, ranks, metricCounts, totalWraps }) {
8395
const doubleTextPercentile = percentiles["content.double_text_count"];
8496
const percentileContext = doubleTextPercentile !== undefined && doubleTextPercentile !== null && totalWraps > 0
8597
? ` That's more than ${doubleTextPercentile}% of ${totalWraps.toLocaleString()} users.`
@@ -120,6 +132,8 @@ function DoubleTextSection({ content, percentiles, totalWraps }) {
120132
label="Double Texts Sent"
121133
value={content.double_text_count.toLocaleString()}
122134
percentile={percentiles["content.double_text_count"]}
135+
rank={ranks["content.double_text_count"]}
136+
metricTotal={metricCounts["content.double_text_count"]}
123137
totalWraps={totalWraps}
124138
valueStyle={{ fontSize: "2.5rem", color: "#06b6d4" }}
125139
/>
@@ -128,6 +142,8 @@ function DoubleTextSection({ content, percentiles, totalWraps }) {
128142
label="💥 Quadruple Texts (Crash Outs)"
129143
value={content.quadruple_text_count.toLocaleString()}
130144
percentile={percentiles["content.quadruple_text_count"]}
145+
rank={ranks["content.quadruple_text_count"]}
146+
metricTotal={metricCounts["content.quadruple_text_count"]}
131147
totalWraps={totalWraps}
132148
valueStyle={{ fontSize: "2.5rem", color: "#ef4444" }}
133149
/>

web/components/ConversationsSection.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 ConversationsSection({ conversations, percentiles = {}, totalWraps = 0 }) {
3+
export default function ConversationsSection({ conversations, percentiles = {}, ranks = {}, metricCounts = {}, totalWraps = 0 }) {
44
if (!conversations) return null;
55

66
return (
@@ -12,6 +12,8 @@ export default function ConversationsSection({ conversations, percentiles = {},
1212
label="Total Conversations"
1313
value={conversations.total_conversations}
1414
percentile={percentiles["conversations.total_conversations"]}
15+
rank={ranks["conversations.total_conversations"]}
16+
metricTotal={metricCounts["conversations.total_conversations"]}
1517
totalWraps={totalWraps}
1618
valueStyle={{ fontSize: "2rem" }}
1719
/>
@@ -21,6 +23,8 @@ export default function ConversationsSection({ conversations, percentiles = {},
2123
label="Group Chats"
2224
value={conversations.group_chats}
2325
percentile={percentiles["conversations.group_chats"]}
26+
rank={ranks["conversations.group_chats"]}
27+
metricTotal={metricCounts["conversations.group_chats"]}
2428
totalWraps={totalWraps}
2529
valueStyle={{ fontSize: "2rem" }}
2630
/>
@@ -30,6 +34,8 @@ export default function ConversationsSection({ conversations, percentiles = {},
3034
label="1-on-1 Chats"
3135
value={conversations.one_on_one_chats}
3236
percentile={percentiles["conversations.one_on_one_chats"]}
37+
rank={ranks["conversations.one_on_one_chats"]}
38+
metricTotal={metricCounts["conversations.one_on_one_chats"]}
3339
totalWraps={totalWraps}
3440
valueStyle={{ fontSize: "2rem" }}
3541
/>

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 = {}, totalWraps = 0 }) {
3+
export default function GhostSection({ ghosts, percentiles = {}, ranks = {}, metricCounts = {}, totalWraps = 0 }) {
44
if (!ghosts) return null;
55

66
const totalYou = ghosts.people_you_left_hanging || 0;
@@ -26,13 +26,17 @@ export default function GhostSection({ ghosts, percentiles = {}, totalWraps = 0
2626
label="People You Left Hanging"
2727
value={totalYou.toLocaleString()}
2828
percentile={percentiles["ghosts.people_you_left_hanging"]}
29+
rank={ranks["ghosts.people_you_left_hanging"]}
30+
metricTotal={metricCounts["ghosts.people_you_left_hanging"]}
2931
totalWraps={totalWraps}
3032
valueStyle={{ fontSize: "2rem" }}
3133
/>
3234
<StatCard
3335
label="People Who Left You Hanging"
3436
value={totalThem.toLocaleString()}
3537
percentile={percentiles["ghosts.people_who_left_you_hanging"]}
38+
rank={ranks["ghosts.people_who_left_you_hanging"]}
39+
metricTotal={metricCounts["ghosts.people_who_left_you_hanging"]}
3640
totalWraps={totalWraps}
3741
valueStyle={{ fontSize: "2rem" }}
3842
/>
@@ -41,6 +45,8 @@ export default function GhostSection({ ghosts, percentiles = {}, totalWraps = 0
4145
label="Ghost Ratio (You/Them)"
4246
value={ratio.toFixed(2)}
4347
percentile={percentiles["ghosts.ghost_ratio"]}
48+
rank={ranks["ghosts.ghost_ratio"]}
49+
metricTotal={metricCounts["ghosts.ghost_ratio"]}
4450
totalWraps={totalWraps}
4551
valueStyle={{ fontSize: "2rem" }}
4652
/>

web/components/HeroSection.js

Lines changed: 6 additions & 2 deletions
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 = {}, totalWraps = 0, userName = null }) {
3+
export default function HeroSection({ year, volume, percentiles = {}, ranks = {}, metricCounts = {}, totalWraps = 0, userName = null }) {
44
// Capitalize each word in the name (e.g., "griffin tarpenning" → "Griffin Tarpenning")
55
const capitalizeWords = (str) => {
66
if (!str) return str;
@@ -21,21 +21,25 @@ export default function HeroSection({ year, volume, percentiles = {}, totalWraps
2121
return (
2222
<div className="hero">
2323
<h1>
24-
{possessiveName} <span className="gradient-text">{year}</span> in Messages
24+
<span style={{ color: '#fde047' }}>{possessiveName}</span> <span className="gradient-text">{year}</span> in Messages
2525
</h1>
2626

2727
<div className="stats-grid">
2828
<StatCard
2929
label="Messages Sent"
3030
value={volume?.total_sent?.toLocaleString() || 0}
3131
percentile={percentiles["volume.total_sent"]}
32+
rank={ranks["volume.total_sent"]}
33+
metricTotal={metricCounts["volume.total_sent"]}
3234
totalWraps={totalWraps}
3335
/>
3436

3537
<StatCard
3638
label="Messages Received"
3739
value={volume?.total_received?.toLocaleString() || 0}
3840
percentile={percentiles["volume.total_received"]}
41+
rank={ranks["volume.total_received"]}
42+
metricTotal={metricCounts["volume.total_received"]}
3943
totalWraps={totalWraps}
4044
valueStyle={{
4145
background: "linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%)",

web/components/MessageLengthSection.js

Lines changed: 7 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 = {}, totalWraps = 0 }) {
5+
export default function MessageLengthSection({ content, percentiles = {}, ranks = {}, metricCounts = {}, totalWraps = 0 }) {
66
if (!content) return null;
77

88
const hasLengthData =
@@ -21,6 +21,8 @@ export default function MessageLengthSection({ content, percentiles = {}, totalW
2121
label="Avg Length (Sent)"
2222
value={`${Math.round(content.avg_message_length_sent)} chars`}
2323
percentile={percentiles["content.avg_message_length_sent"]}
24+
rank={ranks["content.avg_message_length_sent"]}
25+
metricTotal={metricCounts["content.avg_message_length_sent"]}
2426
totalWraps={totalWraps}
2527
valueStyle={{ fontSize: "2rem" }}
2628
/>
@@ -30,6 +32,8 @@ export default function MessageLengthSection({ content, percentiles = {}, totalW
3032
label="Avg Length (Received)"
3133
value={`${Math.round(content.avg_message_length_received)} chars`}
3234
percentile={percentiles["content.avg_message_length_received"]}
35+
rank={ranks["content.avg_message_length_received"]}
36+
metricTotal={metricCounts["content.avg_message_length_received"]}
3337
totalWraps={totalWraps}
3438
valueStyle={{ fontSize: "2rem" }}
3539
/>
@@ -39,6 +43,8 @@ export default function MessageLengthSection({ content, percentiles = {}, totalW
3943
label="Avg Words (Sent)"
4044
value={`${Math.round(content.avg_word_count_sent)} words`}
4145
percentile={percentiles["content.avg_word_count_sent"]}
46+
rank={ranks["content.avg_word_count_sent"]}
47+
metricTotal={metricCounts["content.avg_word_count_sent"]}
4248
totalWraps={totalWraps}
4349
valueStyle={{ fontSize: "2rem" }}
4450
/>

0 commit comments

Comments
 (0)