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
20 changes: 20 additions & 0 deletions .claude/napkin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Napkin

## Corrections
| Date | Source | What Went Wrong | What To Do Instead |
|------|--------|----------------|-------------------|

## User Preferences
- Hide navbar on full-screen flow pages (invite, dashboard, admin) via `ConditionalHeader` patterns + remove `page-with-header` class

## Patterns That Work
- Convex queries can join related data inline (e.g., activity types + categories in one query)
- `conditional-header.tsx` DASHBOARD_LAYOUT_PATTERNS array controls navbar visibility per route

## Patterns That Don't Work

## Domain Notes
- Scoring configs have types: distance, duration, count, variant
- `page-with-header` CSS class = `pt-16` to offset fixed navbar
- Seed data lives in `packages/backend/actions/seed.ts`
- Schema changes auto-deploy locally via `pnpm dev`
134 changes: 133 additions & 1 deletion apps/web/app/challenges/[id]/admin/emails/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
CheckCircle,
Clock,
Edit2,
Eye,
Layers,
Mail,
Plus,
Save,
Expand Down Expand Up @@ -95,10 +97,13 @@ const triggerInfo: Record<
},
};

type ViewMode = "sequences" | "template";

export default function EmailsAdminPage() {
const params = useParams();
const challengeId = params.id as string;

const [viewMode, setViewMode] = useState<ViewMode>("sequences");
const [selectedEmailId, setSelectedEmailId] = useState<string | null>(null);
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false);
Expand All @@ -123,6 +128,8 @@ export default function EmailsAdminPage() {
challengeId: challengeId as Id<"challenges">,
});

const templatePreview = useQuery(api.queries.emailSequences.getEmailTemplatePreview);

const selectedEmail = useQuery(
api.queries.emailSequences.getById,
selectedEmailId ? { emailSequenceId: selectedEmailId as Id<"emailSequences"> } : "skip"
Expand Down Expand Up @@ -261,7 +268,130 @@ export default function EmailsAdminPage() {
}

return (
<div className="flex h-[calc(100vh-140px)] gap-4">
<div className="flex h-[calc(100vh-140px)] flex-col gap-3">
{/* View Mode Toggle */}
<div className="flex items-center gap-2">
<button
onClick={() => setViewMode("sequences")}
className={cn(
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors",
viewMode === "sequences"
? "bg-amber-500/15 text-amber-400"
: "text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300"
)}
>
<Layers className="h-3.5 w-3.5" />
Email Sequences
</button>
<button
onClick={() => setViewMode("template")}
className={cn(
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors",
viewMode === "template"
? "bg-amber-500/15 text-amber-400"
: "text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300"
)}
>
<Eye className="h-3.5 w-3.5" />
Email Template
</button>
</div>

{/* Template Preview Mode */}
{viewMode === "template" ? (
<div className="flex flex-1 gap-4 overflow-hidden">
{/* Info Panel */}
<div className="flex w-1/3 flex-col gap-3">
<div className="rounded border border-zinc-800 bg-zinc-900 p-4">
<div className="mb-3 flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded bg-amber-500/15">
<Mail className="h-4 w-4 text-amber-400" />
</div>
<div>
<h3 className="text-sm font-medium text-zinc-100">Base Email Template</h3>
<p className="text-[10px] text-zinc-500">Used across all transactional emails</p>
</div>
</div>
<p className="text-xs leading-relaxed text-zinc-400">
This is the shared email template that wraps all transactional emails sent from March Fitness, including invite emails, welcome emails, and weekly recaps.
</p>
</div>

<div className="rounded border border-zinc-800 bg-zinc-900 p-4">
<h4 className="mb-2 text-[10px] font-medium uppercase tracking-wider text-zinc-500">
Template Components
</h4>
<div className="space-y-2">
<div className="rounded border border-zinc-800 bg-zinc-800/50 px-3 py-2">
<div className="text-xs font-medium text-zinc-300">Brand Header</div>
<div className="text-[10px] text-zinc-500">Wordmark + indigo-fuchsia gradient divider</div>
</div>
<div className="rounded border border-zinc-800 bg-zinc-800/50 px-3 py-2">
<div className="text-xs font-medium text-zinc-300">Dark Card</div>
<div className="text-[10px] text-zinc-500">Title, subtitle, content, callouts, and CTA buttons</div>
</div>
<div className="rounded border border-zinc-800 bg-zinc-800/50 px-3 py-2">
<div className="text-xs font-medium text-zinc-300">Footer</div>
<div className="text-[10px] text-zinc-500">Context line + march.fit wordmark link</div>
</div>
</div>
</div>

<div className="rounded border border-zinc-800 bg-zinc-900 p-4">
<h4 className="mb-2 text-[10px] font-medium uppercase tracking-wider text-zinc-500">
Used In
</h4>
<div className="space-y-1.5">
{[
{ name: "Invite Emails", desc: "Sent when users invite friends" },
{ name: "Welcome Email", desc: "Auto-sent on challenge signup" },
{ name: "Weekly Recaps", desc: "Sent after each week" },
{ name: "Challenge Complete", desc: "Sent when challenge ends" },
].map((item) => (
<div key={item.name} className="flex items-center gap-2 rounded bg-zinc-800/50 px-3 py-1.5">
<CheckCircle className="h-3 w-3 flex-shrink-0 text-emerald-400" />
<div>
<div className="text-xs text-zinc-300">{item.name}</div>
<div className="text-[10px] text-zinc-500">{item.desc}</div>
</div>
</div>
))}
</div>
</div>
</div>

{/* Template Preview */}
<div className="flex flex-1 flex-col overflow-hidden rounded border border-zinc-800 bg-zinc-900">
<div className="flex items-center justify-between border-b border-zinc-800 px-3 py-2">
<div className="flex items-center gap-2">
<h2 className="text-sm font-medium text-zinc-100">Template Preview</h2>
<span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[10px] font-medium text-emerald-400">
Active
</span>
</div>
<div className="text-[10px] text-zinc-500">
All emails use this layout
</div>
</div>
<div className="flex-1 overflow-hidden bg-[#09090b]">
{templatePreview ? (
<iframe
srcDoc={templatePreview.html}
className="h-full w-full border-0"
title="Email Template Preview"
sandbox=""
/>
) : (
<div className="flex h-full items-center justify-center text-zinc-500">
Loading template...
</div>
)}
</div>
</div>
</div>
) : (
/* Email Sequences Mode */
<div className="flex flex-1 gap-4 overflow-hidden">
{/* Left Column - Email List */}
<div className="flex w-1/2 flex-col">
{/* Header */}
Expand Down Expand Up @@ -781,6 +911,8 @@ export default function EmailsAdminPage() {
</div>
)}
</div>
</div>
)}
</div>
);
}
5 changes: 3 additions & 2 deletions apps/web/app/challenges/[id]/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { api } from "@repo/backend";
import type { Id } from "@repo/backend/_generated/dataModel";

import { ActivityFeed } from "@/components/dashboard/activity-feed";
import { OnboardingCard } from "@/components/dashboard/onboarding-card";
import { type ChallengeSummary } from "@/components/dashboard/challenge-realtime-context";
import { getCurrentUser } from "@/lib/auth";
import { isAuthenticated } from "@/lib/server-auth";
Expand Down Expand Up @@ -61,7 +62,6 @@ export default async function ChallengeDashboardPage({
0,
Math.ceil((dateOnlyToUtcMs(challenge.endDate) - now.getTime()) / DAY_IN_MS),
);

const initialSummary: ChallengeSummary = {
stats: {
...stats,
Expand Down Expand Up @@ -89,7 +89,8 @@ export default async function ChallengeDashboardPage({
currentUser={user}
initialSummary={initialSummary}
>
<div className="mx-auto max-w-2xl px-4 py-6">
<div className="mx-auto max-w-2xl px-4 py-6 space-y-4">
<OnboardingCard challengeId={challenge.id} userId={user._id} />
<ActivityFeed challengeId={challenge.id} />
</div>
</DashboardLayoutWrapper>
Expand Down
Loading
Loading