Add contribution field visualization to user profile sidebar

Seto Elkahfi committed Apr 15, 2026 at 22:47 UTC c893ad6bf3618f4fa4eaab5aa5b7b89350639775
1 file changed +89 -13
app/views/users/show.html.erb
+89 -13
@@ -1,5 +1,29 @@
1 <% content_for :title, @profile_user.display_name_or_username %>
2
3 +<%
4 + contribution_columns = [
5 + [0, 0, 1, 0, 1, 0, 0],
6 + [0, 1, 1, 0, 1, 1, 0],
7 + [0, 1, 2, 1, 1, 1, 0],
8 + [0, 2, 1, 2, 2, 1, 0],
9 + [1, 1, 2, 2, 1, 2, 1],
10 + [0, 2, 2, 3, 2, 2, 1],
11 + [1, 2, 3, 2, 3, 2, 1],
12 + [1, 2, 3, 3, 2, 3, 2],
13 + [1, 3, 2, 3, 3, 3, 2],
14 + [2, 3, 3, 4, 3, 4, 3],
15 + [2, 3, 4, 4, 4, 4, 3],
16 + [3, 4, 4, 4, 4, 4, 4]
17 + ]
18 + contribution_palette = {
19 + 0 => { text: "#3a3d30", background: "#151713", border: "#23261e" },
20 + 1 => { text: "#6f7552", background: "#1b1f18", border: "#343828" },
21 + 2 => { text: "#9ea16b", background: "#23281d", border: "#4d5537" },
22 + 3 => { text: "#c9cc8a", background: "#2d3323", border: "#71794f" },
23 + 4 => { text: "#f1f3b2", background: "#394128", border: "#9fa96a" }
24 + }
25 +%>
26 +
27 <div class="max-w-6xl mx-auto px-4 sm:px-6 py-10"
28 data-controller="tabs"
29 data-tabs-active-value="<%= params[:tab] || 'feed' %>">
@@ -8,20 +32,72 @@
32
33 <%# ── Sidebar ── %>
34 <aside class="lg:col-span-1">
11 - <img src="<%= @profile_user.avatar_url_or_default %>"
12 - alt="<%= @profile_user.display_name_or_username %>"
13 - class="w-full max-w-[200px] rounded-full border border-surface-600 mb-4">
14 - <h1 class="text-xl font-semibold text-gray-100"><%= @profile_user.display_name_or_username %></h1>
15 - <p class="text-gray-400 text-sm mb-4"><%= @profile_user.username %></p>
16 -
17 - <% if @profile_user.email.present? %>
18 - <div class="flex items-center gap-2 text-sm text-gray-400">
19 - <svg class="w-4 h-4 shrink-0" viewBox="0 0 16 16" fill="currentColor">
20 - <path d="M1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25v-8.5C0 2.784.784 2 1.75 2ZM1.5 12.251c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V5.809L8.38 9.397a.75.75 0 0 1-.76 0L1.5 5.809v6.442Zm13-8.181v-.32a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25v.32L8 7.88Z"/>
21 - </svg>
22 - <%= @profile_user.email %>
35 + <div class="grid grid-cols-[minmax(0,1fr)_132px] gap-4 items-start lg:block">
36 + <div class="min-w-0">
37 + <img src="<%= @profile_user.avatar_url_or_default %>"
38 + alt="<%= @profile_user.display_name_or_username %>"
39 + class="w-full max-w-[200px] rounded-full border border-surface-600 mb-4">
40 + <h1 class="text-xl font-semibold text-gray-100"><%= @profile_user.display_name_or_username %></h1>
41 + <p class="text-gray-400 text-sm mb-4"><%= @profile_user.username %></p>
42 +
43 + <% if @profile_user.email.present? %>
44 + <div class="flex items-center gap-2 text-sm text-gray-400">
45 + <svg class="w-4 h-4 shrink-0" viewBox="0 0 16 16" fill="currentColor">
46 + <path d="M1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25v-8.5C0 2.784.784 2 1.75 2ZM1.5 12.251c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25V5.809L8.38 9.397a.75.75 0 0 1-.76 0L1.5 5.809v6.442Zm13-8.181v-.32a.25.25 0 0 0-.25-.25H1.75a.25.25 0 0 0-.25.25v.32L8 7.88Z"/>
47 + </svg>
48 + <%= @profile_user.email %>
49 + </div>
50 + <% end %>
51 + </div>
52 +
53 + <div class="card p-3 lg:hidden">
54 + <div class="flex items-center justify-between mb-2">
55 + <span class="text-[10px] uppercase tracking-[0.22em] text-gray-500">Activity</span>
56 + <span class="text-[10px] text-[#f1f3b2]">Latest peak</span>
57 + </div>
58 + <div class="grid gap-1 justify-end" style="grid-template-columns: repeat(<%= contribution_columns.length %>, minmax(0, 1fr));">
59 + <% contribution_columns.each do |week| %>
60 + <div class="grid gap-1" style="grid-template-rows: repeat(7, minmax(0, 1fr));">
61 + <% week.each do |level| %>
62 + <% colors = contribution_palette[level] %>
63 + <div class="h-5 w-2.5 rounded-full border flex items-center justify-center font-semibold text-[11px] leading-none"
64 + style="color: <%= colors[:text] %>; background-color: <%= colors[:background] %>; border-color: <%= colors[:border] %>;">
65 + <span style="writing-mode: vertical-rl; transform: rotate(180deg);">u</span>
66 + </div>
67 + <% end %>
68 + </div>
69 + <% end %>
70 + </div>
71 + <p class="mt-2 text-[10px] leading-4 text-gray-500">Ascending recent streak, rightmost column is the freshest contribution.</p>
72 </div>
24 - <% end %>
73 + </div>
74 +
75 + <div class="card mt-6 p-5 hidden lg:block">
76 + <div class="flex items-center justify-between mb-4">
77 + <div>
78 + <p class="text-[10px] uppercase tracking-[0.22em] text-gray-500">Contribution Field</p>
79 + <p class="mt-1 text-xs text-gray-400">PO-inspired ramp using vertical <span class="font-mono text-[#f1f3b2]">u</span> marks.</p>
80 + </div>
81 + <span class="badge-gray text-[10px]">12 weeks</span>
82 + </div>
83 + <div class="grid gap-1" style="grid-template-columns: repeat(<%= contribution_columns.length %>, minmax(0, 1fr));">
84 + <% contribution_columns.each do |week| %>
85 + <div class="grid gap-1" style="grid-template-rows: repeat(7, minmax(0, 1fr));">
86 + <% week.each do |level| %>
87 + <% colors = contribution_palette[level] %>
88 + <div class="h-7 rounded-full border flex items-center justify-center font-semibold text-sm leading-none"
89 + style="color: <%= colors[:text] %>; background-color: <%= colors[:background] %>; border-color: <%= colors[:border] %>;">
90 + <span style="writing-mode: vertical-rl; transform: rotate(180deg);">u</span>
91 + </div>
92 + <% end %>
93 + </div>
94 + <% end %>
95 + </div>
96 + <div class="mt-4 flex items-center justify-between text-[10px] uppercase tracking-[0.18em] text-gray-500">
97 + <span>Older</span>
98 + <span>Recent high</span>
99 + </div>
100 + </div>
101 </aside>
102
103 <%# ── Main ── %>