Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion web/app/WebsitePostHogProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
// through the server-component boundary, and before the first SDK capture.
import { PostHogProvider } from '@posthog/next/pages';
import type { ReactNode } from 'react';
import { dropAgentDashboardAnalytics } from '../lib/site-analytics';
import { maskSignupNetworkRequest, sanitizeSignupAnalytics } from '../lib/signup-analytics-privacy';

export function WebsitePostHogProvider({ apiKey, host, children }: { apiKey: string; host: string; children: ReactNode }) {
return <PostHogProvider apiKey={apiKey} clientOptions={{
api_host: host, autocapture: true, capture_exceptions: true, capture_heatmaps: true,
capture_pageview: false, capture_pageleave: true,
session_recording: { maskAllInputs: true, blockSelector: '.ph-sensitive', maskCapturedNetworkRequestFn: maskSignupNetworkRequest },
before_send: sanitizeSignupAnalytics,
before_send: [dropAgentDashboardAnalytics, sanitizeSignupAnalytics],
}}>{children}</PostHogProvider>;
}
220 changes: 220 additions & 0 deletions web/app/u/[handle]/dashboard/dashboard.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
.page {
background: #0d2638;
}

.page :is(a, button):focus-visible {
outline: 2px solid var(--flows-accent-strong);
outline-offset: 4px;
}

.main {
max-width: 960px;
margin: 0 auto;
padding: clamp(96px, 12vw, 136px) 16px 96px;
}

.header {
margin-bottom: 32px;
}

.headline {
margin: 0;
font-family: var(--font-heading), sans-serif;
color: var(--flows-text);
font-size: clamp(2rem, 4.5vw, 3rem);
font-weight: 500;
}

.handle {
display: inline-block;
margin: 12px 0 0;
border: 1px solid #47708d;
border-radius: 999px;
padding: 4px 12px;
color: #b9ddf5;
font-family: var(--font-mono, ui-monospace), monospace;
font-size: 13px;
}

.note,
.explain,
.generated,
.muted {
color: var(--flows-muted);
line-height: 1.6;
font-size: 14px;
}

.note {
margin: 16px 0 0;
}

.notice {
border: 1px solid #8f655d;
border-radius: 12px;
padding: 20px;
background: #291d1d;
color: #ffd1c8;
line-height: 1.6;
}

.notice p {
margin: 0;
}

.notice p + p {
margin-top: 8px;
}

.notice code {
font-family: var(--font-mono, ui-monospace), monospace;
font-size: 13px;
overflow-wrap: anywhere;
}

.toggle {
display: flex;
gap: 8px;
margin-bottom: 32px;
}

.toggle a {
border: 1px solid #405c72;
border-radius: 999px;
padding: 6px 14px;
color: var(--flows-muted);
font-size: 14px;
text-decoration: none;
}

.toggle .toggleActive {
border-color: var(--flows-accent);
color: var(--flows-text);
background: #16384f;
}

.section {
margin-bottom: 48px;
}

.sectionHeading {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 12px;
margin: 0 0 8px;
font-family: var(--font-heading), sans-serif;
color: var(--flows-text);
font-size: clamp(1.4rem, 3vw, 1.8rem);
font-weight: 500;
}

.estimated {
border: 1px solid #8f7a4d;
border-radius: 999px;
padding: 2px 10px;
color: #f3dca6;
font-family: var(--font-mono, ui-monospace), monospace;
font-size: 12px;
font-weight: 400;
}

.generated {
margin: 0 0 20px;
}

.tiles {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
margin: 0;
}

.tile {
border: 1px solid #2c4a60;
border-radius: 12px;
padding: 14px 16px;
background: #102e43;
}

.tile dt {
color: var(--flows-muted);
font-size: 13px;
}

.tile dd {
margin: 6px 0 0;
color: var(--flows-text);
font-size: 1.5rem;
font-variant-numeric: tabular-nums;
}

.explain {
margin: 12px 0 32px;
}

.block {
margin-bottom: 32px;
}

.blockHeading {
margin: 0 0 12px;
color: var(--flows-text);
font-size: 1rem;
font-weight: 600;
}

.breakdowns {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 0 24px;
}

.tableWrap {
overflow-x: auto;
}

.table {
width: 100%;
border-collapse: collapse;
color: var(--flows-text);
font-size: 14px;
}

.table th,
.table td {
border-bottom: 1px solid #23415a;
padding: 8px 10px;
text-align: left;
font-weight: 400;
white-space: nowrap;
}

.table thead th {
color: var(--flows-muted);
font-size: 13px;
}

.table .num {
text-align: right;
font-variant-numeric: tabular-nums;
}

.barCell {
display: flex;
align-items: center;
gap: 8px;
min-width: 140px;
}

.bar {
display: block;
height: 10px;
min-width: 0;
border-radius: 3px;
background: var(--flows-accent);
}

.barValue {
font-variant-numeric: tabular-nums;
}
22 changes: 22 additions & 0 deletions web/app/u/[handle]/dashboard/exchange/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { dashboardConfirmPage, handleDashboardExchange } from '../../../../../lib/agent-dashboard';

// `/u/<handle>/dashboard?grant=...` is rewritten here by next.config.mjs
// (beforeFiles, keyed on the grant query), so no page, layout, or analytics
// code runs for a grant-bearing request. GET only shows a static confirmation
// page; the single-use grant is redeemed by its same-origin POST, so link
// scanners and prefetchers cannot burn it. See lib/agent-dashboard.ts.
export const dynamic = 'force-dynamic';

type RouteContext = {
params: Promise<{ handle: string }>;
};

export async function GET(request: Request, { params }: RouteContext) {
Comment thread
khaliqgant marked this conversation as resolved.
const { handle } = await params;
return dashboardConfirmPage(request, handle);
}

export async function POST(request: Request, { params }: RouteContext) {
const { handle } = await params;
return handleDashboardExchange(request, handle);
}
Loading
Loading