Skip to main content

max / makenotwork

Wire up tips UI: CSS, settings toggle, dashboard display - Tip button CSS (tip-section, tip-form, tip-toggle, tip-submit) - Settings toggle for tips_enabled and notify_tip in notification preferences - Dashboard payments tab shows tips received with total, count, and message - TipReceived view type for template rendering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-04-23 03:22 UTC
Commit: 0a038d802aa26aaf9eae82c829cb6a01e30d20f2
Parent: 99c743d
6 files changed, +161 insertions, -0 deletions
@@ -150,10 +150,28 @@ pub(in crate::routes::pages::dashboard) async fn dashboard_tab_payments(
150 150 .await?;
151 151 let transactions = super::super::collect_transactions(incoming_txs, outgoing_txs);
152 152
153 + // Tips
154 + let db_tips = db::tips::get_tips_received(&state.db, session_user.id, 20, 0).await?;
155 + let tips_total_cents = db::tips::total_tips_received(&state.db, session_user.id).await?;
156 + let tips_count = db::tips::count_tips_received(&state.db, session_user.id).await?;
157 + let tips_received: Vec<TipReceived> = db_tips
158 + .iter()
159 + .map(|t| TipReceived {
160 + date: t.created_at.format("%Y-%m-%d").to_string(),
161 + tipper_name: t.tipper_display_name.clone()
162 + .unwrap_or_else(|| t.tipper_username.clone()),
163 + amount: helpers::format_price(t.amount_cents),
164 + message: t.message.clone(),
165 + })
166 + .collect();
167 +
153 168 Ok(UserPaymentsTabTemplate {
154 169 user,
155 170 payout_summary,
156 171 transactions,
172 + tips_received,
173 + tips_total: helpers::format_revenue(tips_total_cents),
174 + tips_count,
157 175 })
158 176 }
159 177
@@ -189,6 +189,9 @@ pub struct UserPaymentsTabTemplate {
189 189 pub user: User,
190 190 pub payout_summary: Option<PayoutSummary>,
191 191 pub transactions: Vec<Transaction>,
192 + pub tips_received: Vec<TipReceived>,
193 + pub tips_total: String,
194 + pub tips_count: i64,
192 195 }
193 196
194 197 #[derive(Template)]
@@ -363,6 +363,15 @@ pub struct Transaction {
363 363 pub details: String,
364 364 }
365 365
366 + /// Tip received for payments tab
367 + #[derive(Clone)]
368 + pub struct TipReceived {
369 + pub date: String,
370 + pub tipper_name: String,
371 + pub amount: String,
372 + pub message: Option<String>,
373 + }
374 +
366 375 /// Project card for dashboard
367 376 #[derive(Clone)]
368 377 pub struct ProjectCard {
@@ -1172,6 +1172,14 @@ footer {
1172 1172 }
1173 1173
1174 1174 .centered-page .tagline {
1175 + margin-bottom: 0.25rem;
1176 + }
1177 +
1178 + .subtagline {
1179 + font-family: var(--font-mono);
1180 + color: var(--text-muted);
1181 + text-align: center;
1182 + font-size: 0.9rem;
1175 1183 margin-bottom: 1rem;
1176 1184 }
1177 1185
@@ -3966,3 +3974,86 @@ textarea:focus-visible {
3966 3974 opacity: 0.8;
3967 3975 border-top: 1px solid var(--border-color, #ccc);
3968 3976 }
3977 +
3978 + /* ── Tips ── */
3979 +
3980 + .tip-section {
3981 + margin-top: 1rem;
3982 + text-align: center;
3983 + }
3984 +
3985 + .tip-toggle {
3986 + font-family: var(--font-mono);
3987 + font-size: 0.85rem;
3988 + padding: 0.5rem 1.5rem;
3989 + background: none;
3990 + border: 1px solid var(--text);
3991 + color: var(--text);
3992 + cursor: pointer;
3993 + text-decoration: none;
3994 + display: inline-block;
3995 + }
3996 +
3997 + .tip-toggle:hover {
3998 + background: var(--text);
3999 + color: var(--background);
4000 + }
4001 +
4002 + .tip-form {
4003 + margin-top: 1rem;
4004 + display: flex;
4005 + flex-direction: column;
4006 + gap: 0.75rem;
4007 + max-width: 300px;
4008 + margin-left: auto;
4009 + margin-right: auto;
4010 + text-align: left;
4011 + }
4012 +
4013 + .tip-form.hidden {
4014 + display: none;
4015 + }
4016 +
4017 + .tip-amount-row {
4018 + display: flex;
4019 + align-items: center;
4020 + gap: 0.25rem;
4021 + }
4022 +
4023 + .tip-amount-row .pricing-currency {
4024 + font-size: 1.2rem;
4025 + opacity: 0.7;
4026 + }
4027 +
4028 + .tip-amount-input {
4029 + width: 80px;
4030 + font-size: 1.2rem;
4031 + padding: 0.4rem 0.5rem;
4032 + border: 1px solid var(--border);
4033 + background: var(--light-background);
4034 + font-family: var(--font-mono);
4035 + }
4036 +
4037 + .tip-message-input {
4038 + width: 100%;
4039 + font-size: 0.85rem;
4040 + padding: 0.5rem;
4041 + border: 1px solid var(--border);
4042 + background: var(--light-background);
4043 + font-family: var(--font-body);
4044 + resize: vertical;
4045 + }
4046 +
4047 + .tip-submit {
4048 + font-family: var(--font-mono);
4049 + font-size: 0.85rem;
4050 + padding: 0.5rem 1rem;
4051 + background: var(--text);
4052 + color: var(--background);
4053 + border: none;
4054 + cursor: pointer;
4055 + }
4056 +
4057 + .tip-submit:hover {
4058 + opacity: 0.85;
4059 + }
@@ -315,6 +315,16 @@
315 315 {% if user.notify_issues %}checked{% endif %}>
316 316 <label for="notify-issues">Email me about new issues and comments on my repos</label>
317 317 </div>
318 + <div class="checkbox-group" style="margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--border);">
319 + <input type="checkbox" id="tips-enabled" name="tips_enabled" value="on"
320 + {% if user.tips_enabled %}checked{% endif %}>
321 + <label for="tips-enabled">Accept tips on my profile and project pages</label>
322 + </div>
323 + <div class="checkbox-group">
324 + <input type="checkbox" id="notify-tip" name="notify_tip" value="on"
325 + {% if user.notify_tip %}checked{% endif %}>
326 + <label for="notify-tip">Email me when I receive a tip</label>
327 + </div>
318 328 {% endif %}
319 329 <div id="preferences-result"></div>
320 330 <button type="submit" class="primary" style="margin-top: 1rem;">Save Preferences</button>
@@ -202,6 +202,36 @@
202 202 </div>
203 203 {% endif %}
204 204
205 + {% if tips_count > 0 %}
206 + <details class="form-section" open>
207 + <summary><h2>Tips Received ({{ tips_count }})</h2></summary>
208 + <div style="background: var(--surface-muted); padding: 1.25rem; margin-bottom: 1.5rem;">
209 + <div style="font-size: 1.25rem; font-weight: bold; margin-bottom: 0.5rem;">{{ tips_total }} total</div>
210 + <div style="font-size: 0.85rem; opacity: 0.7;">{{ tips_count }} tips received</div>
211 + </div>
212 + <table class="data-table">
213 + <thead>
214 + <tr>
215 + <th>Date</th>
216 + <th>From</th>
217 + <th>Amount</th>
218 + <th>Message</th>
219 + </tr>
220 + </thead>
221 + <tbody>
222 + {% for tip in tips_received %}
223 + <tr>
224 + <td>{{ tip.date }}</td>
225 + <td>{{ tip.tipper_name }}</td>
226 + <td>{{ tip.amount }}</td>
227 + <td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">{% if let Some(msg) = tip.message %}{{ msg }}{% else %}<span style="opacity: 0.4;">-</span>{% endif %}</td>
228 + </tr>
229 + {% endfor %}
230 + </tbody>
231 + </table>
232 + </details>
233 + {% endif %}
234 +
205 235 <div class="filter-controls">
206 236 <div class="filter-group">
207 237 <select name="type"