Telepathic Collect: link-first, flattened hierarchy, embedded leaderboard

Core insight: The primary object is the LINK, not the appeal.
Aaisha doesn't think 'manage appeals' — she thinks 'share my link'.

## Collect page (/dashboard/collect) — complete rewrite
- Flattened hierarchy: single-appeal orgs see links directly (no card to click)
- Multi-appeal orgs: quiet appeal switcher at top, links below
- Inline link creation: just type a name + press Enter (no dialog)
- Quick preset buttons: 'Table 1', 'WhatsApp Group', 'Instagram', etc.
- Share buttons are THE primary CTA on every link card (Copy, WhatsApp, Email, Share)
- Each link shows: clicks, pledges, amount raised, conversion rate
- Embedded mini-leaderboard when 3+ links have pledges
- Contextual tips when pledges < 5 ('give each volunteer their own link')
- New appeal creation is inline, auto-creates 'Main link'

## Appeal detail page (/dashboard/events/[id]) — brand redesign
- Sharp edges, gap-px grids, typography-as-hero
- Same link card component with share-first design
- Embedded leaderboard section
- Inline link creation (same as Collect)
- Clone appeal button
- Appeal details in collapsed <details> (context, not hero)
- Download all QR codes link
- Public progress page link

## Leaderboard page — brand redesign
- Total raised as hero number (dark section)
- Progress bars relative to leader
- Medal badges for top 3
- Conversion rate badges
- Auto-refresh every 10 seconds (live event mode)

## Route cleanup
- /dashboard/events re-exports /dashboard/collect (backward compat)
- Old events/page.tsx removed (was duplicate)

5 files changed, 3 pages redesigned
This commit is contained in:
2026-03-04 21:13:32 +08:00
parent 6fb97e1461
commit a9b3b70dfc
14 changed files with 1680 additions and 556 deletions

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Filament\Widgets;
use App\Models\Donation;
use Filament\Widgets\ChartWidget;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
class DonationChartWidget extends ChartWidget
{
protected static ?string $heading = 'Daily Donations — Last 30 Days';
protected static ?int $sort = 2;
protected int | string | array $columnSpan = 'full';
protected static ?string $maxHeight = '220px';
protected function getData(): array
{
$confirmedScope = fn (Builder $q) => $q->whereNotNull('confirmed_at');
$days = collect(range(29, 0))->map(fn ($i) => now()->subDays($i)->format('Y-m-d'));
$donations = Donation::whereHas('donationConfirmation', $confirmedScope)
->where('created_at', '>=', now()->subDays(30)->startOfDay())
->selectRaw('DATE(created_at) as date, SUM(amount) / 100 as total')
->groupByRaw('DATE(created_at)')
->pluck('total', 'date');
return [
'datasets' => [
[
'label' => 'Revenue (£)',
'data' => $days->map(fn ($d) => round($donations[$d] ?? 0, 2))->toArray(),
'borderColor' => '#10b981',
'backgroundColor' => 'rgba(16, 185, 129, 0.1)',
'fill' => true,
'tension' => 0.3,
],
],
'labels' => $days->map(fn ($d) => Carbon::parse($d)->format('d M'))->toArray(),
];
}
protected function getType(): string
{
return 'line';
}
}