mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
Daily Briefing
This commit is contained in:
parent
fef19c6b0f
commit
ab8e45c76a
@ -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({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DailyBriefOverviewCard brief={TODAYS_DAILY_BRIEF} projectId={projectId} />
|
||||
|
||||
<div
|
||||
ref={gridContainerRef}
|
||||
className={cn(
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { DesignBadge, DesignButton, DesignCard } from "@/components/design-components";
|
||||
import { Link } from "@/components/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ArrowRightIcon, ChartBarIcon, CheckCircleIcon, LightningIcon, SparkleIcon } from "@phosphor-icons/react";
|
||||
import type { DailyBrief, DailyBriefBullet, DailyBriefSuggestion } from "./mock-data";
|
||||
|
||||
export function SuggestionActionCard({
|
||||
suggestion,
|
||||
completed,
|
||||
onApply,
|
||||
compact = false,
|
||||
}: {
|
||||
suggestion: DailyBriefSuggestion,
|
||||
completed: boolean,
|
||||
onApply: (suggestionId: string) => void,
|
||||
compact?: boolean,
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-completed={completed ? "true" : "false"}
|
||||
className={cn(
|
||||
"rounded-2xl border border-blue-500/15 bg-blue-500/[0.06] p-4 ring-1 ring-blue-500/10 transition-all duration-150 hover:transition-none",
|
||||
"data-[completed=true]:border-emerald-500/20 data-[completed=true]:bg-emerald-500/[0.08] data-[completed=true]:ring-emerald-500/15",
|
||||
completed && "scale-[0.99]",
|
||||
compact && "p-3",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0 space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DesignBadge
|
||||
label={completed ? "Completed" : "AI suggestion"}
|
||||
color={completed ? "green" : "blue"}
|
||||
icon={completed ? CheckCircleIcon : SparkleIcon}
|
||||
size="sm"
|
||||
/>
|
||||
<span className="text-xs font-semibold text-foreground">
|
||||
{suggestion.title}
|
||||
</span>
|
||||
</div>
|
||||
<p className={cn("text-sm text-muted-foreground", compact && "text-xs")}>
|
||||
{suggestion.summary}
|
||||
</p>
|
||||
<p className="text-xs font-medium text-foreground">
|
||||
{suggestion.impact}
|
||||
</p>
|
||||
</div>
|
||||
<DesignButton
|
||||
size="sm"
|
||||
variant={completed ? "secondary" : "default"}
|
||||
disabled={completed}
|
||||
onClick={() => 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",
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
{completed ? <CheckCircleIcon className="h-3.5 w-3.5" weight="fill" /> : null}
|
||||
{completed ? "Completed" : suggestion.actionLabel}
|
||||
</span>
|
||||
</DesignButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<DesignCard
|
||||
title="Today's Daily Brief"
|
||||
subtitle="Daily notes for Demo Project"
|
||||
icon={SparkleIcon}
|
||||
gradient="purple"
|
||||
actions={
|
||||
<DesignBadge
|
||||
label={`${suggestionCount} suggestions`}
|
||||
color="purple"
|
||||
icon={LightningIcon}
|
||||
size="sm"
|
||||
/>
|
||||
}
|
||||
contentClassName="space-y-4"
|
||||
>
|
||||
<div className="grid gap-4 lg:grid-cols-[1fr_16rem]">
|
||||
<div className="min-w-0 space-y-3">
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">{brief.dateLabel}</span>
|
||||
<span aria-hidden>/</span>
|
||||
<span>{brief.readTime}</span>
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold tracking-tight text-foreground">
|
||||
{brief.title}
|
||||
</h2>
|
||||
</div>
|
||||
<ul className="space-y-2 text-sm leading-6 text-muted-foreground">
|
||||
{previewBullets.map((bullet) => (
|
||||
<li key={bullet.label} className="flex gap-2">
|
||||
<span aria-hidden className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-purple-500/70" />
|
||||
<span>
|
||||
<span className="font-medium text-foreground">{bullet.label}:</span>{" "}
|
||||
{bullet.text}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<DesignButton asChild variant="outline" size="sm" className="w-fit">
|
||||
<Link href={`/projects/${encodeURIComponent(projectId)}/daily-brief`}>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
Read full brief
|
||||
<ArrowRightIcon className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</Link>
|
||||
</DesignButton>
|
||||
</div>
|
||||
<div className="rounded-2xl bg-foreground/[0.04] p-4 ring-1 ring-foreground/[0.08]">
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<ChartBarIcon className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Briefing signals
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
{coreMetrics.map((metric) => (
|
||||
<div key={metric.label}>
|
||||
<div className="text-lg font-semibold tabular-nums text-foreground">{metric.value}</div>
|
||||
<div className="text-xs text-muted-foreground">{metric.label} / {metric.delta}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DesignCard>
|
||||
);
|
||||
}
|
||||
@ -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] : [])));
|
||||
}
|
||||
@ -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 (
|
||||
<div className="grid gap-x-6 gap-y-3 border-l border-foreground/[0.08] pl-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{metrics.map((metric) => (
|
||||
<div key={metric.label} className="min-w-0">
|
||||
<div className="text-xs font-medium text-muted-foreground">{metric.label}</div>
|
||||
<div className="mt-1 text-2xl font-semibold tabular-nums text-foreground">{metric.value}</div>
|
||||
<div className={cn(
|
||||
"mt-1 text-xs font-medium",
|
||||
metric.tone === "good" && "text-emerald-700 dark:text-emerald-300",
|
||||
metric.tone === "watch" && "text-amber-700 dark:text-amber-300",
|
||||
metric.tone === "neutral" && "text-muted-foreground",
|
||||
)}>
|
||||
{metric.delta}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UsersBlock({ users }: { users: DailyBriefImportantUser[] }) {
|
||||
return (
|
||||
<div className="grid gap-x-6 gap-y-4 border-l border-foreground/[0.08] pl-4 xl:grid-cols-3">
|
||||
{users.map((user) => (
|
||||
<div key={user.email} className="min-w-0">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-purple-500/10 text-purple-700 dark:text-purple-300">
|
||||
<UserCircleIcon className="h-5 w-5" weight="fill" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold text-foreground">{user.name}</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{user.company} / {user.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-muted-foreground">
|
||||
{user.signal}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VisualBlock({ visual }: { visual: DailyBriefVisual }) {
|
||||
return (
|
||||
<div className="border-l border-foreground/[0.08] pl-4">
|
||||
<div className="max-w-3xl overflow-hidden rounded-xl border border-dashed border-foreground/[0.12] bg-foreground/[0.025]">
|
||||
<div className="relative h-40 bg-gradient-to-br from-purple-500/12 via-blue-500/8 to-cyan-500/12">
|
||||
<div className="absolute inset-x-6 bottom-6 grid grid-cols-5 items-end gap-2 opacity-80">
|
||||
{[42, 58, 36, 74, 91].map((height) => (
|
||||
<div key={height} className="rounded-t bg-foreground/20" style={{ height: height * 0.7 }} />
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute left-4 top-4 inline-flex items-center gap-2 rounded-full bg-background/75 px-2.5 py-1 text-xs font-medium text-foreground shadow-sm ring-1 ring-foreground/[0.08] backdrop-blur-xl">
|
||||
<ImageIcon className="h-3.5 w-3.5" />
|
||||
{visual.kind === "chart" ? "Mock chart" : visual.kind === "map" ? "Mock journey map" : "Mock image"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm font-semibold text-foreground">{visual.title}</div>
|
||||
<p className="mt-1 text-xs leading-5 text-muted-foreground">{visual.caption}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DailyBriefBlockRenderer({
|
||||
block,
|
||||
completedSuggestionIds,
|
||||
onApplySuggestion,
|
||||
}: {
|
||||
block: DailyBriefBlock,
|
||||
completedSuggestionIds: Set<string>,
|
||||
onApplySuggestion: (suggestionId: string) => void,
|
||||
}) {
|
||||
switch (block.type) {
|
||||
case "bullets": {
|
||||
return (
|
||||
<ul className="max-w-4xl space-y-2.5 border-l border-foreground/[0.08] pl-4 text-sm leading-6 text-muted-foreground">
|
||||
{block.bullets.map((bullet) => (
|
||||
<li key={bullet.label} className="relative">
|
||||
<span aria-hidden className="absolute -left-[1.18rem] top-2.5 h-px w-3 bg-foreground/[0.16]" />
|
||||
<div className="text-sm font-semibold text-foreground">{bullet.label}</div>
|
||||
<div className="text-sm text-muted-foreground">{bullet.text}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
case "metrics": {
|
||||
return <MetricsBlock metrics={block.metrics} />;
|
||||
}
|
||||
case "users": {
|
||||
return <UsersBlock users={block.users} />;
|
||||
}
|
||||
case "visual": {
|
||||
return <VisualBlock visual={block.visual} />;
|
||||
}
|
||||
case "suggestion": {
|
||||
return (
|
||||
<SuggestionActionCard
|
||||
suggestion={block.suggestion}
|
||||
completed={completedSuggestionIds.has(block.suggestion.id)}
|
||||
onApply={onApplySuggestion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function BriefTreeSection({
|
||||
section,
|
||||
sectionIndex,
|
||||
completedSuggestionIds,
|
||||
onApplySuggestion,
|
||||
}: {
|
||||
section: DailyBrief["sections"][number],
|
||||
sectionIndex: number,
|
||||
completedSuggestionIds: Set<string>,
|
||||
onApplySuggestion: (suggestionId: string) => void,
|
||||
}) {
|
||||
return (
|
||||
<section className="relative grid grid-cols-[2.25rem_1fr] gap-3">
|
||||
<div aria-hidden className="relative flex justify-center">
|
||||
<div className="absolute bottom-[-1.5rem] top-7 w-px bg-foreground/[0.12]" />
|
||||
<div className="relative z-10 flex h-7 w-7 items-center justify-center rounded-lg bg-purple-500/10 text-xs font-bold text-purple-700 ring-1 ring-purple-500/20 dark:text-purple-300">
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 pb-5">
|
||||
<div className="mb-3">
|
||||
<div className="flex flex-wrap items-baseline gap-2">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-[0.18em] text-purple-700 dark:text-purple-300">
|
||||
Section {sectionIndex + 1}
|
||||
</span>
|
||||
<h3 className="text-lg font-semibold tracking-tight text-foreground">
|
||||
{section.heading}
|
||||
</h3>
|
||||
</div>
|
||||
{section.intro != null && (
|
||||
<p className="mt-1 max-w-3xl text-sm leading-6 text-muted-foreground">
|
||||
{section.intro}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-4 pl-3">
|
||||
{section.blocks.map((block, index) => (
|
||||
<DailyBriefBlockRenderer
|
||||
key={`${section.heading}-${index}`}
|
||||
block={block}
|
||||
completedSuggestionIds={completedSuggestionIds}
|
||||
onApplySuggestion={onApplySuggestion}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function BriefTree({
|
||||
brief,
|
||||
completedSuggestionIds,
|
||||
onApplySuggestion,
|
||||
}: {
|
||||
brief: DailyBrief,
|
||||
completedSuggestionIds: Set<string>,
|
||||
onApplySuggestion: (suggestionId: string) => void,
|
||||
}) {
|
||||
return (
|
||||
<article className="space-y-1">
|
||||
<header className="grid grid-cols-[2.25rem_1fr] gap-3">
|
||||
<div aria-hidden className="relative flex justify-center">
|
||||
<div className="absolute top-8 h-5 w-px bg-foreground/[0.12]" />
|
||||
<div className="relative z-10 flex h-8 w-8 items-center justify-center rounded-xl bg-foreground text-background shadow-sm">
|
||||
<SparkleIcon className="h-4 w-4" weight="fill" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 pb-3">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>{brief.dayLabel}</span>
|
||||
<span aria-hidden>/</span>
|
||||
<span>{brief.readTime}</span>
|
||||
</div>
|
||||
<h2 className="mt-1 max-w-4xl text-2xl font-semibold tracking-tight text-foreground">
|
||||
{brief.title}
|
||||
</h2>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{brief.sections.map((section, sectionIndex) => (
|
||||
<BriefTreeSection
|
||||
key={section.heading}
|
||||
section={section}
|
||||
sectionIndex={sectionIndex}
|
||||
completedSuggestionIds={completedSuggestionIds}
|
||||
onApplySuggestion={onApplySuggestion}
|
||||
/>
|
||||
))}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PageClient() {
|
||||
const allSuggestions = useMemo(() => getAllDailyBriefSuggestions(), []);
|
||||
const [completedSuggestionIds, setCompletedSuggestionIds] = useState<Set<string>>(() => new Set());
|
||||
const [enabledBriefDays, setEnabledBriefDays] = useState<Set<WeekdayId>>(() => 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 (
|
||||
<PageLayout
|
||||
title="Daily Brief"
|
||||
description="Short daily notes for Demo Project. Mock data only for the team presentation."
|
||||
actions={
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<DesignBadge
|
||||
label={`${completedCount}/${allSuggestions.length} completed`}
|
||||
color={allSuggestionsCompleted ? "green" : "blue"}
|
||||
icon={allSuggestionsCompleted ? CheckCircleIcon : LightningIcon}
|
||||
size="sm"
|
||||
/>
|
||||
<DesignButton
|
||||
size="sm"
|
||||
onClick={applyAllSuggestions}
|
||||
disabled={allSuggestionsCompleted}
|
||||
variant={allSuggestionsCompleted ? "secondary" : "default"}
|
||||
className={cn(
|
||||
"transition-all duration-150 hover:transition-none",
|
||||
allSuggestionsCompleted && "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
|
||||
)}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
{allSuggestionsCompleted ? <CheckCircleIcon className="h-3.5 w-3.5" weight="fill" /> : null}
|
||||
{allSuggestionsCompleted ? "All suggestions completed" : "Apply all suggestions"}
|
||||
</span>
|
||||
</DesignButton>
|
||||
<DesignDialog
|
||||
trigger={
|
||||
<DesignButton variant="outline" size="icon" aria-label="Configure Daily Brief schedule">
|
||||
<GearIcon className="h-4 w-4" />
|
||||
</DesignButton>
|
||||
}
|
||||
icon={GearIcon}
|
||||
title="Daily Brief schedule"
|
||||
description="Choose which days should create a new brief. This is mock UI for now."
|
||||
size="md"
|
||||
footer={
|
||||
<DesignDialogClose asChild>
|
||||
<DesignButton size="sm">Done</DesignButton>
|
||||
</DesignDialogClose>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-2xl bg-foreground/[0.04] p-3 text-sm text-muted-foreground ring-1 ring-foreground/[0.08]">
|
||||
Current schedule: <span className="font-medium text-foreground">{enabledDayLabel || "No days selected"}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
{WEEKDAYS.map((day) => (
|
||||
<label
|
||||
key={day.id}
|
||||
className="flex cursor-pointer items-center gap-2 rounded-xl bg-foreground/[0.035] px-3 py-2 text-sm font-medium text-foreground ring-1 ring-foreground/[0.08] transition-colors duration-150 hover:bg-foreground/[0.06] hover:transition-none"
|
||||
>
|
||||
<Checkbox
|
||||
checked={enabledBriefDays.has(day.id)}
|
||||
onCheckedChange={(checked) => toggleBriefDay(day.id, checked === true)}
|
||||
/>
|
||||
{day.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DesignDialog>
|
||||
</div>
|
||||
}
|
||||
fillWidth
|
||||
wrapHeaderInCard
|
||||
>
|
||||
<div className="grid gap-4 lg:grid-cols-[16rem_1fr]">
|
||||
<DesignCard title="Archive" icon={SparkleIcon} gradient="purple" contentClassName="space-y-2">
|
||||
{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 (
|
||||
<a
|
||||
key={brief.id}
|
||||
href={`#${brief.id}`}
|
||||
className="block rounded-xl p-3 transition-colors duration-150 hover:bg-foreground/[0.04] hover:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/[0.16]"
|
||||
>
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<span className="truncate text-xs font-semibold text-foreground">{brief.dayLabel}</span>
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground">{brief.readTime}</span>
|
||||
</div>
|
||||
<p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
|
||||
{brief.title}
|
||||
</p>
|
||||
{suggestionCount > 0 && (
|
||||
<div className="mt-2 text-[10px] font-medium text-blue-700 dark:text-blue-300">
|
||||
{suggestionLabel}
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</DesignCard>
|
||||
|
||||
<div className="space-y-4">
|
||||
{DAILY_BRIEFS.map((brief) => (
|
||||
<DesignCard
|
||||
key={brief.id}
|
||||
id={brief.id}
|
||||
title={brief.dateLabel}
|
||||
subtitle={brief.summary}
|
||||
icon={SparkleIcon}
|
||||
gradient="purple"
|
||||
actions={
|
||||
<div className="flex flex-wrap justify-end gap-1.5">
|
||||
{brief.tags.map((tag) => (
|
||||
<DesignBadge key={tag} label={tag} color="purple" size="sm" />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
contentClassName="space-y-6"
|
||||
>
|
||||
<BriefTree
|
||||
brief={brief}
|
||||
completedSuggestionIds={completedSuggestionIds}
|
||||
onApplySuggestion={applySuggestion}
|
||||
/>
|
||||
</DesignCard>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
import PageClient from "./page-client";
|
||||
|
||||
export const metadata = {
|
||||
title: "Daily Brief",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<PageClient />
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
<NavItem
|
||||
item={dailyBriefItem}
|
||||
onClick={onNavigate}
|
||||
href={`/projects/${projectId}${dailyBriefItem.href}`}
|
||||
isCollapsed={isCollapsed}
|
||||
/>
|
||||
<NavItem
|
||||
item={usersItem}
|
||||
onClick={onNavigate}
|
||||
|
||||
@ -282,6 +282,14 @@ const PROJECT_SHORTCUTS: ProjectShortcutDefinition[] = [
|
||||
href: "/users",
|
||||
keywords: ["users", "user", "people", "members", "accounts"],
|
||||
},
|
||||
{
|
||||
id: "navigation/daily-brief",
|
||||
icon: SparkleIcon,
|
||||
label: "Daily Brief",
|
||||
description: "Navigation",
|
||||
href: "/daily-brief",
|
||||
keywords: ["daily brief", "briefing", "ai brief", "gtm assistant", "recommendations", "suggestions"],
|
||||
},
|
||||
{
|
||||
id: "navigation/dashboards",
|
||||
icon: ChartBarIcon,
|
||||
|
||||
@ -2974,7 +2974,7 @@
|
||||
"/emails/send-email": {
|
||||
"post": {
|
||||
"summary": "Send email",
|
||||
"description": "Send an email to a list of users. The content field should contain either {html} for HTML emails, {template_id, variables} for template-based emails, or {draft_id} for a draft email.",
|
||||
"description": "Send an email to a list of users (user_ids), all users (all_users), or arbitrary email addresses (emails). The content field should contain either {html} for HTML emails, {template_id, variables} for template-based emails, or {draft_id} for a draft email.",
|
||||
"parameters": [],
|
||||
"tags": [
|
||||
"Emails"
|
||||
@ -2995,11 +2995,12 @@
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"user_id"
|
||||
]
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -2934,7 +2934,7 @@
|
||||
"/emails/send-email": {
|
||||
"post": {
|
||||
"summary": "Send email",
|
||||
"description": "Send an email to a list of users. The content field should contain either {html} for HTML emails, {template_id, variables} for template-based emails, or {draft_id} for a draft email.",
|
||||
"description": "Send an email to a list of users (user_ids), all users (all_users), or arbitrary email addresses (emails). The content field should contain either {html} for HTML emails, {template_id, variables} for template-based emails, or {draft_id} for a draft email.",
|
||||
"parameters": [],
|
||||
"tags": [
|
||||
"Emails"
|
||||
@ -2955,11 +2955,12 @@
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"user_id"
|
||||
]
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user