Skip to content

Commit 148156a

Browse files
fix(dashboard): minor follow-up fixes
1 parent 9a91429 commit 148156a

2 files changed

Lines changed: 136 additions & 19 deletions

File tree

app/dashboard/page.tsx

Lines changed: 106 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,79 @@ import { PromptCollection } from "@/components/PromptCollection";
1212
import { usePrompts } from "@/lib/hooks/usePrompts";
1313
import { useAuth } from "@/components/AuthProvider";
1414

15+
const ONBOARDING_KEY = (userId: string) => `closednote_onboarded_${userId}`;
16+
17+
function WelcomeBanner({ userName, onDismiss }: { userName: string; onDismiss: () => void }) {
18+
return (
19+
<div className="max-w-xl mx-auto py-16 px-4">
20+
<p className="text-xs font-medium tracking-widest text-neutral-400 dark:text-neutral-500 uppercase mb-3">
21+
You&apos;re in
22+
</p>
23+
<h1 className="text-3xl font-semibold text-neutral-900 dark:text-neutral-100 mb-3">
24+
Welcome{userName ? `, ${userName}` : ""}.
25+
</h1>
26+
<p className="text-neutral-500 dark:text-neutral-400 text-sm leading-relaxed mb-10">
27+
closedNote is where you save, version, and refine your prompts. Here&apos;s how to get started.
28+
</p>
29+
30+
<ol className="space-y-5 mb-10">
31+
<li className="flex gap-4">
32+
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 text-xs font-semibold flex items-center justify-center">
33+
1
34+
</span>
35+
<div>
36+
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">Save a prompt</p>
37+
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
38+
Paste or type any prompt, give it a title, and hit save. Takes 10 seconds.
39+
</p>
40+
</div>
41+
</li>
42+
<li className="flex gap-4">
43+
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 text-xs font-semibold flex items-center justify-center">
44+
2
45+
</span>
46+
<div>
47+
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">Edit freely, versions save automatically</p>
48+
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
49+
Every time you update a prompt, the previous version is kept. Restore any version in one click.
50+
</p>
51+
</div>
52+
</li>
53+
<li className="flex gap-4">
54+
<span className="flex-shrink-0 w-7 h-7 rounded-full bg-neutral-100 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 text-xs font-semibold flex items-center justify-center">
55+
3
56+
</span>
57+
<div>
58+
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">Refine with AI when you need it</p>
59+
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-0.5">
60+
Open any prompt and ask AI to improve it. Add your OpenAI key in Settings to unlock GPT-4o.
61+
</p>
62+
</div>
63+
</li>
64+
</ol>
65+
66+
<div className="flex items-center gap-4">
67+
<Link
68+
href="/prompts/new"
69+
onClick={onDismiss}
70+
className="inline-flex items-center px-5 py-2.5 bg-neutral-900 hover:bg-neutral-800 dark:bg-neutral-100 dark:hover:bg-neutral-200 dark:text-neutral-900 text-white font-medium rounded-full transition-colors text-sm"
71+
>
72+
+ Create your first prompt
73+
</Link>
74+
<button
75+
onClick={onDismiss}
76+
className="text-sm text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300 transition-colors"
77+
>
78+
Skip for now
79+
</button>
80+
</div>
81+
</div>
82+
);
83+
}
84+
1585
function DashboardContent() {
1686
const { prompts: allPrompts, loading, error } = usePrompts();
17-
const { loading: authLoading } = useAuth();
87+
const { user, loading: authLoading } = useAuth();
1888
const [searchQuery, setSearchQuery] = useState("");
1989
const [filters, setFilters] = useState<{
2090
query: string;
@@ -23,6 +93,7 @@ function DashboardContent() {
2393
const [activeCollection, setActiveCollection] = useState<
2494
string | undefined
2595
>();
96+
const [showWelcome, setShowWelcome] = useState(false);
2697

2798
useEffect(() => {
2899
if (typeof window !== "undefined") {
@@ -36,6 +107,21 @@ function DashboardContent() {
36107
setFilters((prev) => ({ ...prev, query: searchQuery }));
37108
}, [searchQuery]);
38109

110+
// Show welcome only once per account, on first login with no prompts
111+
useEffect(() => {
112+
if (!user || authLoading || loading) return;
113+
if (allPrompts.length > 0) return;
114+
const key = ONBOARDING_KEY(user.id);
115+
if (!localStorage.getItem(key)) {
116+
setShowWelcome(true);
117+
}
118+
}, [user, authLoading, loading, allPrompts.length]);
119+
120+
function dismissWelcome() {
121+
if (user) localStorage.setItem(ONBOARDING_KEY(user.id), "1");
122+
setShowWelcome(false);
123+
}
124+
39125
const filteredPrompts = useMemo(() => {
40126
return filterPrompts(allPrompts, {
41127
query: filters.query || undefined,
@@ -80,18 +166,25 @@ function DashboardContent() {
80166
))}
81167
</div>
82168
) : allPrompts.length === 0 ? (
83-
<div className="max-w-sm mx-auto text-center py-20">
84-
<p className="text-neutral-900 dark:text-neutral-100 font-medium mb-2">No prompts yet</p>
85-
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-6">
86-
Create your first one and it&apos;ll show up here.
87-
</p>
88-
<Link
89-
href="/prompts/new"
90-
className="inline-flex items-center px-5 py-2.5 bg-neutral-900 hover:bg-neutral-800 dark:bg-neutral-100 dark:hover:bg-neutral-200 dark:text-neutral-900 text-white font-medium rounded-full transition-colors text-sm"
91-
>
92-
+ New prompt
93-
</Link>
94-
</div>
169+
showWelcome ? (
170+
<WelcomeBanner
171+
userName={user?.displayName?.split(" ")[0] ?? ""}
172+
onDismiss={dismissWelcome}
173+
/>
174+
) : (
175+
<div className="max-w-sm mx-auto text-center py-20">
176+
<p className="text-neutral-900 dark:text-neutral-100 font-medium mb-2">No prompts yet</p>
177+
<p className="text-sm text-neutral-500 dark:text-neutral-400 mb-6">
178+
Create your first one and it&apos;ll show up here.
179+
</p>
180+
<Link
181+
href="/prompts/new"
182+
className="inline-flex items-center px-5 py-2.5 bg-neutral-900 hover:bg-neutral-800 dark:bg-neutral-100 dark:hover:bg-neutral-200 dark:text-neutral-900 text-white font-medium rounded-full transition-colors text-sm"
183+
>
184+
+ New prompt
185+
</Link>
186+
</div>
187+
)
95188
) : (
96189
<div className="max-w-5xl mx-auto w-full">
97190
<div>

app/prompts/new/page.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,51 @@
11
"use client";
22

3-
import Link from "next/link";
3+
import { useState, useEffect, useCallback } from "react";
4+
import { useRouter } from "next/navigation";
45
import { PromptForm } from "@/components/PromptForm";
56
import { Header } from "@/components/Header";
67
import { Layout } from "@/components/Layout";
78
import { usePrompts } from "@/lib/hooks/usePrompts";
89

910
export default function NewPromptPage() {
1011
const { prompts } = usePrompts();
12+
const router = useRouter();
13+
const [isDirty, setIsDirty] = useState(false);
14+
15+
// Warn on browser refresh / tab close when form has unsaved content
16+
useEffect(() => {
17+
if (!isDirty) return;
18+
const handler = (e: BeforeUnloadEvent) => {
19+
e.preventDefault();
20+
e.returnValue = "";
21+
};
22+
window.addEventListener("beforeunload", handler);
23+
return () => window.removeEventListener("beforeunload", handler);
24+
}, [isDirty]);
25+
26+
const handleBack = useCallback(() => {
27+
if (isDirty) {
28+
const confirmed = window.confirm(
29+
"You have unsaved changes. Leave without saving?"
30+
);
31+
if (!confirmed) return;
32+
}
33+
router.push("/dashboard");
34+
}, [isDirty, router]);
1135

1236
return (
1337
<Layout header={<Header promptCount={prompts.length} />} sidebar={null}>
1438
<div className="max-w-2xl mx-auto">
15-
<Link
16-
href="/dashboard"
39+
<button
40+
onClick={handleBack}
1741
className="inline-flex items-center text-sm text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 transition-colors mb-8"
1842
>
19-
Back
20-
</Link>
43+
&larr; Back
44+
</button>
2145
<h1 className="text-2xl font-medium text-neutral-900 dark:text-neutral-100 mb-8">
2246
New prompt
2347
</h1>
24-
<PromptForm />
48+
<PromptForm onDirtyChange={setIsDirty} />
2549
</div>
2650
</Layout>
2751
);

0 commit comments

Comments
 (0)