From ab8e45c76a93f0100f4d010667fa2e5350c1606b Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Wed, 8 Jul 2026 11:20:50 -0700 Subject: [PATCH] Daily Briefing --- .../[projectId]/(overview)/metrics-page.tsx | 4 + .../[projectId]/_daily-brief/components.tsx | 148 +++++++ .../[projectId]/_daily-brief/mock-data.ts | 368 +++++++++++++++++ .../[projectId]/daily-brief/page-client.tsx | 386 ++++++++++++++++++ .../projects/[projectId]/daily-brief/page.tsx | 11 + .../projects/[projectId]/sidebar-layout.tsx | 15 + .../src/components/cmdk-commands.tsx | 8 + docs-mintlify/openapi/admin.json | 9 +- docs-mintlify/openapi/server.json | 9 +- 9 files changed, 950 insertions(+), 8 deletions(-) create mode 100644 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/_daily-brief/components.tsx create mode 100644 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/_daily-brief/mock-data.ts create mode 100644 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/daily-brief/page-client.tsx create mode 100644 apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/daily-brief/page.tsx diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx index 7b2153abf..4f5922048 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx @@ -46,6 +46,8 @@ import { LayoutGroup, motion, useReducedMotion, type Transition } from "motion/r import { ErrorBoundary } from "next/dist/client/components/error-boundary"; import { type ElementType, type ReactNode, Suspense, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react"; import { AnalyticsEventLimitBanner } from "../analytics/shared"; +import { DailyBriefOverviewCard } from "../_daily-brief/components"; +import { TODAYS_DAILY_BRIEF } from "../_daily-brief/mock-data"; import { PageLayout } from "../page-layout"; import { useAdminApp, useProjectId } from "../use-admin-app"; import { UserPageMetricCard } from "../users/[userId]/user-page-metric-card"; @@ -1899,6 +1901,8 @@ function MetricsContent({ /> + +
void, + compact?: boolean, +}) { + return ( +
+
+
+
+ + + {suggestion.title} + +
+

+ {suggestion.summary} +

+

+ {suggestion.impact} +

+
+ onApply(suggestion.id)} + className={cn( + "shrink-0 transition-all duration-150 hover:transition-none", + completed && "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", + )} + > + + {completed ? : null} + {completed ? "Completed" : suggestion.actionLabel} + + +
+
+ ); +} + +export function DailyBriefOverviewCard({ + brief, + projectId, +}: { + brief: DailyBrief, + projectId: string, +}) { + const previewBullets: DailyBriefBullet[] = brief.sections.flatMap((section) => section.blocks.flatMap((block) => block.type === "bullets" ? block.bullets : [])).slice(0, 3); + const suggestionCount = brief.sections.flatMap((section) => section.blocks.filter((block) => block.type === "suggestion")).length; + const coreMetrics = brief.sections.flatMap((section) => section.blocks.flatMap((block) => block.type === "metrics" ? block.metrics : [])).slice(0, 2); + + return ( + + } + contentClassName="space-y-4" + > +
+
+
+
+ {brief.dateLabel} + / + {brief.readTime} +
+

+ {brief.title} +

+
+
    + {previewBullets.map((bullet) => ( +
  • + + + {bullet.label}:{" "} + {bullet.text} + +
  • + ))} +
+ + + + Read full brief + + + + +
+
+
+ + + Briefing signals + +
+
+ {coreMetrics.map((metric) => ( +
+
{metric.value}
+
{metric.label} / {metric.delta}
+
+ ))} +
+
+
+
+ ); +} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/_daily-brief/mock-data.ts b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/_daily-brief/mock-data.ts new file mode 100644 index 000000000..39b954ff7 --- /dev/null +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/_daily-brief/mock-data.ts @@ -0,0 +1,368 @@ +export type DailyBriefSuggestion = { + id: string, + title: string, + summary: string, + impact: string, + actionLabel: string, +}; + +export type DailyBriefBullet = { + label: string, + text: string, +}; + +export type DailyBriefMetric = { + label: string, + value: string, + delta: string, + tone: "good" | "watch" | "neutral", +}; + +export type DailyBriefImportantUser = { + name: string, + email: string, + company: string, + signal: string, +}; + +export type DailyBriefVisual = { + title: string, + caption: string, + kind: "chart" | "screenshot" | "map", +}; + +export type DailyBriefBlock = + | { type: "bullets", bullets: DailyBriefBullet[] } + | { type: "metrics", metrics: DailyBriefMetric[] } + | { type: "users", users: DailyBriefImportantUser[] } + | { type: "visual", visual: DailyBriefVisual } + | { type: "suggestion", suggestion: DailyBriefSuggestion }; + +export type DailyBriefSection = { + heading: string, + intro?: string, + blocks: DailyBriefBlock[], +}; + +export type DailyBrief = { + id: string, + dateLabel: string, + dayLabel: string, + title: string, + summary: string, + readTime: string, + tags: string[], + sections: DailyBriefSection[], +}; + +export const DAILY_BRIEFS: DailyBrief[] = [ + { + id: "2026-07-08", + dateLabel: "July 8, 2026", + dayLabel: "Today", + title: "Search traffic is working better than social", + summary: "Short notes on growth, new users, revenue, and suggested next steps.", + readTime: "5 min read", + tags: ["Growth", "Users", "Revenue"], + sections: [ + { + heading: "Executive summary", + intro: "Quick read. The main changes are below.", + blocks: [ + { + type: "bullets", + bullets: [ + { + label: "Search is stronger", + text: "Fewer visits than direct traffic. More sign-ups and first payments.", + }, + { + label: "Team invites matter", + text: "Users who invited teammates finished setup faster.", + }, + { + label: "No urgent issue", + text: "Email and payment health look normal today.", + }, + ], + }, + ], + }, + { + heading: "Core metric growth", + blocks: [ + { + type: "metrics", + metrics: [ + { label: "Qualified sign-ups", value: "128", delta: "+18% vs last week", tone: "good" }, + { label: "Activated projects", value: "42", delta: "+11% vs last week", tone: "good" }, + { label: "First purchase revenue", value: "$675", delta: "+6% vs prior period", tone: "good" }, + { label: "Avg. setup time", value: "14m", delta: "-9% faster", tone: "good" }, + ], + }, + { + type: "visual", + visual: { + kind: "chart", + title: "Setup by source", + caption: "Mock chart: search users reach setup steps sooner than paid social users.", + }, + }, + { + type: "bullets", + bullets: [ + { + label: "Best search terms", + text: "\"auth pricing calculator\" and \"B2B user management SDK\" convert 38% above average.", + }, + { + label: "Social is weaker", + text: "Paid social brings visits, but fewer users finish setup.", + }, + ], + }, + { + type: "suggestion", + suggestion: { + id: "increase-search-budget-auth-pricing", + title: "Move budget to search", + summary: "Increase budget for \"auth pricing calculator\" and \"B2B user management SDK\".", + impact: "+12-18% expected good-fit sign-ups", + actionLabel: "Apply suggestion", + }, + }, + ], + }, + { + heading: "Important recent sign-ups", + blocks: [ + { + type: "users", + users: [ + { + name: "Maya Patel", + email: "maya@northstar.dev", + company: "Northstar Labs", + signal: "Created a project. Invited 3 teammates. Viewed payments.", + }, + { + name: "Ethan Brooks", + email: "ethan@arcforge.ai", + company: "ArcForge AI", + signal: "Came from Google. Read RBAC docs twice. Signed up.", + }, + { + name: "Sofia Nguyen", + email: "sofia@cloudlane.co", + company: "Cloudlane", + signal: "Set up email templates. Sent a test email in 11 minutes.", + }, + ], + }, + { + type: "bullets", + bullets: [ + { + label: "Follow up", + text: "Prioritize accounts that invite teammates and view payments.", + }, + { + label: "Main drop-off", + text: "Users still get stuck before adding a production domain.", + }, + ], + }, + { + type: "suggestion", + suggestion: { + id: "launch-domain-setup-nudge", + title: "Add domain setup nudge", + summary: "Prompt new admins if no domain is added after 20 minutes.", + impact: "+7% expected project activation", + actionLabel: "Apply suggestion", + }, + }, + ], + }, + { + heading: "Extra context", + blocks: [ + { + type: "visual", + visual: { + kind: "screenshot", + title: "Landing page test", + caption: "Mock image: add a short setup proof section near pricing.", + }, + }, + { + type: "bullets", + bullets: [ + { + label: "Revenue", + text: "First purchases mostly come from accounts with multiple active users.", + }, + { + label: "Today", + text: "Focus on search. Fix the domain setup drop-off.", + }, + ], + }, + ], + }, + ], + }, + { + id: "2026-07-07", + dateLabel: "July 7, 2026", + dayLabel: "Yesterday", + title: "Team invites helped users finish setup", + summary: "Projects with early teammate invites were more active.", + readTime: "4 min read", + tags: ["Activation", "Teams"], + sections: [ + { + heading: "Brief", + blocks: [ + { + type: "bullets", + bullets: [ + { + label: "Invites helped", + text: "Teams looked at roles, permissions, and email templates sooner.", + }, + { + label: "Make it early", + text: "Ask for teammate invites earlier in setup.", + }, + ], + }, + ], + }, + { + heading: "Recommended follow-up", + blocks: [ + { + type: "suggestion", + suggestion: { + id: "promote-team-invite-onboarding", + title: "Move team invite up", + summary: "Show team invites earlier for new projects.", + impact: "+9% expected week-one retention", + actionLabel: "Apply suggestion", + }, + }, + ], + }, + ], + }, + { + id: "2026-07-06", + dateLabel: "July 6, 2026", + dayLabel: "Monday", + title: "Email delivery is fine. Template setup needs help.", + summary: "Delivery is stable. Some users edit templates but never send a test.", + readTime: "3 min read", + tags: ["Emails", "Retention"], + sections: [ + { + heading: "Brief", + blocks: [ + { + type: "metrics", + metrics: [ + { label: "Delivered emails", value: "1.8k", delta: "+4%", tone: "good" }, + { label: "Bounce rate", value: "1.2%", delta: "flat", tone: "neutral" }, + { label: "Template stalls", value: "14", delta: "+22%", tone: "watch" }, + ], + }, + { + type: "bullets", + bullets: [ + { + label: "Delivery is fine", + text: "No bounce spike today.", + }, + { + label: "Template setup stalls", + text: "Some users edit templates but never send a test.", + }, + ], + }, + ], + }, + { + heading: "Recommended follow-up", + blocks: [ + { + type: "bullets", + bullets: [ + { + label: "UX nudge", + text: "Show test-send after two template edits.", + }, + ], + }, + ], + }, + ], + }, + { + id: "2026-07-03", + dateLabel: "July 3, 2026", + dayLabel: "Friday", + title: "Pricing visitors want proof faster", + summary: "Pricing visitors often check docs before signing up.", + readTime: "4 min read", + tags: ["Pricing", "Website"], + sections: [ + { + heading: "Brief", + blocks: [ + { + type: "visual", + visual: { + kind: "map", + title: "Pricing to docs", + caption: "Mock journey: users check setup effort before signing up.", + }, + }, + { + type: "bullets", + bullets: [ + { + label: "High intent", + text: "Pricing visitors open setup docs more often.", + }, + { + label: "Missing proof", + text: "Add setup proof near pricing.", + }, + ], + }, + ], + }, + { + heading: "Recommended follow-up", + blocks: [ + { + type: "suggestion", + suggestion: { + id: "add-pricing-implementation-proof", + title: "Add setup proof", + summary: "Add a short setup block to the pricing page.", + impact: "+5% expected pricing-to-sign-up conversion", + actionLabel: "Apply suggestion", + }, + }, + ], + }, + ], + }, +]; + +export const TODAYS_DAILY_BRIEF = DAILY_BRIEFS[0]; + +export function getAllDailyBriefSuggestions(): DailyBriefSuggestion[] { + return DAILY_BRIEFS.flatMap((brief) => brief.sections.flatMap((section) => section.blocks.flatMap((block) => block.type === "suggestion" ? [block.suggestion] : []))); +} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/daily-brief/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/daily-brief/page-client.tsx new file mode 100644 index 000000000..ca5b71f86 --- /dev/null +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/daily-brief/page-client.tsx @@ -0,0 +1,386 @@ +"use client"; + +import { DesignBadge, DesignButton, DesignCard, DesignDialog, DesignDialogClose } from "@/components/design-components"; +import { Checkbox } from "@/components/ui"; +import { cn } from "@/lib/utils"; +import { CheckCircleIcon, GearIcon, ImageIcon, LightningIcon, SparkleIcon, UserCircleIcon } from "@phosphor-icons/react"; +import { useMemo, useState } from "react"; +import { SuggestionActionCard } from "../_daily-brief/components"; +import { DAILY_BRIEFS, getAllDailyBriefSuggestions, type DailyBrief, type DailyBriefBlock, type DailyBriefImportantUser, type DailyBriefMetric, type DailyBriefVisual } from "../_daily-brief/mock-data"; +import { PageLayout } from "../page-layout"; + +const WEEKDAYS = [ + { id: "mon", label: "Mo" }, + { id: "tue", label: "Tu" }, + { id: "wed", label: "We" }, + { id: "thu", label: "Th" }, + { id: "fri", label: "Fr" }, + { id: "sat", label: "Sa" }, + { id: "sun", label: "Su" }, +] as const; + +type WeekdayId = typeof WEEKDAYS[number]["id"]; + +function MetricsBlock({ metrics }: { metrics: DailyBriefMetric[] }) { + return ( +
+ {metrics.map((metric) => ( +
+
{metric.label}
+
{metric.value}
+
+ {metric.delta} +
+
+ ))} +
+ ); +} + +function UsersBlock({ users }: { users: DailyBriefImportantUser[] }) { + return ( +
+ {users.map((user) => ( +
+
+
+ +
+
+
{user.name}
+
{user.company} / {user.email}
+
+
+

+ {user.signal} +

+
+ ))} +
+ ); +} + +function VisualBlock({ visual }: { visual: DailyBriefVisual }) { + return ( +
+
+
+
+ {[42, 58, 36, 74, 91].map((height) => ( +
+ ))} +
+
+ + {visual.kind === "chart" ? "Mock chart" : visual.kind === "map" ? "Mock journey map" : "Mock image"} +
+
+
+
{visual.title}
+

{visual.caption}

+
+
+
+ ); +} + +function DailyBriefBlockRenderer({ + block, + completedSuggestionIds, + onApplySuggestion, +}: { + block: DailyBriefBlock, + completedSuggestionIds: Set, + onApplySuggestion: (suggestionId: string) => void, +}) { + switch (block.type) { + case "bullets": { + return ( +
    + {block.bullets.map((bullet) => ( +
  • + +
    {bullet.label}
    +
    {bullet.text}
    +
  • + ))} +
+ ); + } + case "metrics": { + return ; + } + case "users": { + return ; + } + case "visual": { + return ; + } + case "suggestion": { + return ( + + ); + } + } +} + +function BriefTreeSection({ + section, + sectionIndex, + completedSuggestionIds, + onApplySuggestion, +}: { + section: DailyBrief["sections"][number], + sectionIndex: number, + completedSuggestionIds: Set, + onApplySuggestion: (suggestionId: string) => void, +}) { + return ( +
+
+
+
+ + +
+
+
+
+
+ + Section {sectionIndex + 1} + +

+ {section.heading} +

+
+ {section.intro != null && ( +

+ {section.intro} +

+ )} +
+
+ {section.blocks.map((block, index) => ( + + ))} +
+
+
+ ); +} + +function BriefTree({ + brief, + completedSuggestionIds, + onApplySuggestion, +}: { + brief: DailyBrief, + completedSuggestionIds: Set, + onApplySuggestion: (suggestionId: string) => void, +}) { + return ( +
+
+
+
+
+ +
+
+
+
+ {brief.dayLabel} + / + {brief.readTime} +
+

+ {brief.title} +

+
+
+ + {brief.sections.map((section, sectionIndex) => ( + + ))} +
+ ); +} + +export default function PageClient() { + const allSuggestions = useMemo(() => getAllDailyBriefSuggestions(), []); + const [completedSuggestionIds, setCompletedSuggestionIds] = useState>(() => new Set()); + const [enabledBriefDays, setEnabledBriefDays] = useState>(() => new Set(["mon", "tue", "wed", "thu", "fri"])); + const completedCount = allSuggestions.filter((suggestion) => completedSuggestionIds.has(suggestion.id)).length; + const allSuggestionsCompleted = allSuggestions.length > 0 && completedCount === allSuggestions.length; + const enabledDayLabel = WEEKDAYS.filter((day) => enabledBriefDays.has(day.id)).map((day) => day.label).join(", "); + + const applySuggestion = (suggestionId: string) => { + setCompletedSuggestionIds((previous) => { + const next = new Set(previous); + next.add(suggestionId); + return next; + }); + }; + + const applyAllSuggestions = () => { + setCompletedSuggestionIds(new Set(allSuggestions.map((suggestion) => suggestion.id))); + }; + + const toggleBriefDay = (dayId: WeekdayId, enabled: boolean) => { + setEnabledBriefDays((previous) => { + const next = new Set(previous); + if (enabled) { + next.add(dayId); + } else { + next.delete(dayId); + } + return next; + }); + }; + + return ( + + + + + {allSuggestionsCompleted ? : null} + {allSuggestionsCompleted ? "All suggestions completed" : "Apply all suggestions"} + + + + + + } + icon={GearIcon} + title="Daily Brief schedule" + description="Choose which days should create a new brief. This is mock UI for now." + size="md" + footer={ + + Done + + } + > +
+
+ Current schedule: {enabledDayLabel || "No days selected"} +
+
+ {WEEKDAYS.map((day) => ( + + ))} +
+
+
+
+ } + fillWidth + wrapHeaderInCard + > +
+ + {DAILY_BRIEFS.map((brief) => { + const suggestionCount = brief.sections.flatMap((section) => section.blocks.filter((block) => block.type === "suggestion")).length; + const suggestionLabel = `${suggestionCount} suggested ${suggestionCount === 1 ? "action" : "actions"}`; + return ( + +
+ {brief.dayLabel} + {brief.readTime} +
+

+ {brief.title} +

+ {suggestionCount > 0 && ( +
+ {suggestionLabel} +
+ )} +
+ ); + })} +
+ +
+ {DAILY_BRIEFS.map((brief) => ( + + {brief.tags.map((tag) => ( + + ))} +
+ } + contentClassName="space-y-6" + > + + + ))} +
+
+ + ); +} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/daily-brief/page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/daily-brief/page.tsx new file mode 100644 index 000000000..7a9a9b442 --- /dev/null +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/daily-brief/page.tsx @@ -0,0 +1,11 @@ +import PageClient from "./page-client"; + +export const metadata = { + title: "Daily Brief", +}; + +export default function Page() { + return ( + + ); +} diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx index fe4de87bc..3e802f9fd 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx @@ -35,6 +35,7 @@ import { ListIcon, PlusIcon, SidebarIcon, + SparkleIcon, UsersIcon, type Icon as PhosphorIcon, } from "@phosphor-icons/react"; @@ -101,6 +102,14 @@ const usersItem: Item = { type: "item", }; +const dailyBriefItem: Item = { + name: "Daily Brief", + href: "/daily-brief", + regex: /^\/projects\/[^\/]+\/daily-brief(\/.*)?$/, + icon: SparkleIcon, + type: "item", +}; + const dashboardsItem: Item = { name: "Dashboards", href: "/dashboards", @@ -552,6 +561,12 @@ function SidebarContent({ href={`/projects/${projectId}${overviewItem.href}`} isCollapsed={isCollapsed} /> +