Skip to content

Commit f51d5a5

Browse files
authored
Merge branch 'main' into claude/add-media-scoring-XkPF3
2 parents ba0b309 + 890dad0 commit f51d5a5

27 files changed

Lines changed: 1994 additions & 275 deletions

.claude/napkin.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Napkin
2+
3+
## Corrections
4+
| Date | Source | What Went Wrong | What To Do Instead |
5+
|------|--------|----------------|-------------------|
6+
7+
## User Preferences
8+
- Hide navbar on full-screen flow pages (invite, dashboard, admin) via `ConditionalHeader` patterns + remove `page-with-header` class
9+
10+
## Patterns That Work
11+
- Convex queries can join related data inline (e.g., activity types + categories in one query)
12+
- `conditional-header.tsx` DASHBOARD_LAYOUT_PATTERNS array controls navbar visibility per route
13+
14+
## Patterns That Don't Work
15+
16+
## Domain Notes
17+
- Scoring configs have types: distance, duration, count, variant
18+
- `page-with-header` CSS class = `pt-16` to offset fixed navbar
19+
- Seed data lives in `packages/backend/actions/seed.ts`
20+
- Schema changes auto-deploy locally via `pnpm dev`

apps/web/app/challenges/[id]/admin/emails/page.tsx

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
CheckCircle,
1010
Clock,
1111
Edit2,
12+
Eye,
13+
Layers,
1214
Mail,
1315
Plus,
1416
Save,
@@ -95,10 +97,13 @@ const triggerInfo: Record<
9597
},
9698
};
9799

100+
type ViewMode = "sequences" | "template";
101+
98102
export default function EmailsAdminPage() {
99103
const params = useParams();
100104
const challengeId = params.id as string;
101105

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

131+
const templatePreview = useQuery(api.queries.emailSequences.getEmailTemplatePreview);
132+
126133
const selectedEmail = useQuery(
127134
api.queries.emailSequences.getById,
128135
selectedEmailId ? { emailSequenceId: selectedEmailId as Id<"emailSequences"> } : "skip"
@@ -261,7 +268,130 @@ export default function EmailsAdminPage() {
261268
}
262269

263270
return (
264-
<div className="flex h-[calc(100vh-140px)] gap-4">
271+
<div className="flex h-[calc(100vh-140px)] flex-col gap-3">
272+
{/* View Mode Toggle */}
273+
<div className="flex items-center gap-2">
274+
<button
275+
onClick={() => setViewMode("sequences")}
276+
className={cn(
277+
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors",
278+
viewMode === "sequences"
279+
? "bg-amber-500/15 text-amber-400"
280+
: "text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300"
281+
)}
282+
>
283+
<Layers className="h-3.5 w-3.5" />
284+
Email Sequences
285+
</button>
286+
<button
287+
onClick={() => setViewMode("template")}
288+
className={cn(
289+
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors",
290+
viewMode === "template"
291+
? "bg-amber-500/15 text-amber-400"
292+
: "text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300"
293+
)}
294+
>
295+
<Eye className="h-3.5 w-3.5" />
296+
Email Template
297+
</button>
298+
</div>
299+
300+
{/* Template Preview Mode */}
301+
{viewMode === "template" ? (
302+
<div className="flex flex-1 gap-4 overflow-hidden">
303+
{/* Info Panel */}
304+
<div className="flex w-1/3 flex-col gap-3">
305+
<div className="rounded border border-zinc-800 bg-zinc-900 p-4">
306+
<div className="mb-3 flex items-center gap-2">
307+
<div className="flex h-8 w-8 items-center justify-center rounded bg-amber-500/15">
308+
<Mail className="h-4 w-4 text-amber-400" />
309+
</div>
310+
<div>
311+
<h3 className="text-sm font-medium text-zinc-100">Base Email Template</h3>
312+
<p className="text-[10px] text-zinc-500">Used across all transactional emails</p>
313+
</div>
314+
</div>
315+
<p className="text-xs leading-relaxed text-zinc-400">
316+
This is the shared email template that wraps all transactional emails sent from March Fitness, including invite emails, welcome emails, and weekly recaps.
317+
</p>
318+
</div>
319+
320+
<div className="rounded border border-zinc-800 bg-zinc-900 p-4">
321+
<h4 className="mb-2 text-[10px] font-medium uppercase tracking-wider text-zinc-500">
322+
Template Components
323+
</h4>
324+
<div className="space-y-2">
325+
<div className="rounded border border-zinc-800 bg-zinc-800/50 px-3 py-2">
326+
<div className="text-xs font-medium text-zinc-300">Brand Header</div>
327+
<div className="text-[10px] text-zinc-500">Wordmark + indigo-fuchsia gradient divider</div>
328+
</div>
329+
<div className="rounded border border-zinc-800 bg-zinc-800/50 px-3 py-2">
330+
<div className="text-xs font-medium text-zinc-300">Dark Card</div>
331+
<div className="text-[10px] text-zinc-500">Title, subtitle, content, callouts, and CTA buttons</div>
332+
</div>
333+
<div className="rounded border border-zinc-800 bg-zinc-800/50 px-3 py-2">
334+
<div className="text-xs font-medium text-zinc-300">Footer</div>
335+
<div className="text-[10px] text-zinc-500">Context line + march.fit wordmark link</div>
336+
</div>
337+
</div>
338+
</div>
339+
340+
<div className="rounded border border-zinc-800 bg-zinc-900 p-4">
341+
<h4 className="mb-2 text-[10px] font-medium uppercase tracking-wider text-zinc-500">
342+
Used In
343+
</h4>
344+
<div className="space-y-1.5">
345+
{[
346+
{ name: "Invite Emails", desc: "Sent when users invite friends" },
347+
{ name: "Welcome Email", desc: "Auto-sent on challenge signup" },
348+
{ name: "Weekly Recaps", desc: "Sent after each week" },
349+
{ name: "Challenge Complete", desc: "Sent when challenge ends" },
350+
].map((item) => (
351+
<div key={item.name} className="flex items-center gap-2 rounded bg-zinc-800/50 px-3 py-1.5">
352+
<CheckCircle className="h-3 w-3 flex-shrink-0 text-emerald-400" />
353+
<div>
354+
<div className="text-xs text-zinc-300">{item.name}</div>
355+
<div className="text-[10px] text-zinc-500">{item.desc}</div>
356+
</div>
357+
</div>
358+
))}
359+
</div>
360+
</div>
361+
</div>
362+
363+
{/* Template Preview */}
364+
<div className="flex flex-1 flex-col overflow-hidden rounded border border-zinc-800 bg-zinc-900">
365+
<div className="flex items-center justify-between border-b border-zinc-800 px-3 py-2">
366+
<div className="flex items-center gap-2">
367+
<h2 className="text-sm font-medium text-zinc-100">Template Preview</h2>
368+
<span className="rounded bg-emerald-500/20 px-1.5 py-0.5 text-[10px] font-medium text-emerald-400">
369+
Active
370+
</span>
371+
</div>
372+
<div className="text-[10px] text-zinc-500">
373+
All emails use this layout
374+
</div>
375+
</div>
376+
<div className="flex-1 overflow-hidden bg-[#09090b]">
377+
{templatePreview ? (
378+
<iframe
379+
srcDoc={templatePreview.html}
380+
className="h-full w-full border-0"
381+
title="Email Template Preview"
382+
sandbox=""
383+
/>
384+
) : (
385+
<div className="flex h-full items-center justify-center text-zinc-500">
386+
Loading template...
387+
</div>
388+
)}
389+
</div>
390+
</div>
391+
</div>
392+
) : (
393+
/* Email Sequences Mode */
394+
<div className="flex flex-1 gap-4 overflow-hidden">
265395
{/* Left Column - Email List */}
266396
<div className="flex w-1/2 flex-col">
267397
{/* Header */}
@@ -781,6 +911,8 @@ export default function EmailsAdminPage() {
781911
</div>
782912
)}
783913
</div>
914+
</div>
915+
)}
784916
</div>
785917
);
786918
}

apps/web/app/challenges/[id]/dashboard/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { api } from "@repo/backend";
44
import type { Id } from "@repo/backend/_generated/dataModel";
55

66
import { ActivityFeed } from "@/components/dashboard/activity-feed";
7+
import { OnboardingCard } from "@/components/dashboard/onboarding-card";
78
import { type ChallengeSummary } from "@/components/dashboard/challenge-realtime-context";
89
import { getCurrentUser } from "@/lib/auth";
910
import { isAuthenticated } from "@/lib/server-auth";
@@ -61,7 +62,6 @@ export default async function ChallengeDashboardPage({
6162
0,
6263
Math.ceil((dateOnlyToUtcMs(challenge.endDate) - now.getTime()) / DAY_IN_MS),
6364
);
64-
6565
const initialSummary: ChallengeSummary = {
6666
stats: {
6767
...stats,
@@ -89,7 +89,8 @@ export default async function ChallengeDashboardPage({
8989
currentUser={user}
9090
initialSummary={initialSummary}
9191
>
92-
<div className="mx-auto max-w-2xl px-4 py-6">
92+
<div className="mx-auto max-w-2xl px-4 py-6 space-y-4">
93+
<OnboardingCard challengeId={challenge.id} userId={user._id} />
9394
<ActivityFeed challengeId={challenge.id} />
9495
</div>
9596
</DashboardLayoutWrapper>

0 commit comments

Comments
 (0)