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>
6 files changed,
+161 insertions,
-0 deletions
| 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 |
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 |
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 |
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 |
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 |
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 |
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"
|