11"use client" ;
22
33import { Suspense } from "react" ;
4- import { useState , useMemo , useEffect } from "react" ;
4+ import { useState , useMemo , useEffect , useCallback } from "react" ;
55import Link from "next/link" ;
6+ import { useSearchParams , useRouter } from "next/navigation" ;
67import { groupPromptsByCollection , filterPrompts } from "@/lib/promptData" ;
7- import { PromptModel } from "@/lib/types" ;
8+ import { Prompt , PromptModel } from "@/lib/types" ;
89import { Header } from "@/components/Header" ;
910import { Sidebar } from "@/components/Sidebar" ;
1011import { Layout } from "@/components/Layout" ;
@@ -14,6 +15,8 @@ import { useAuth } from "@/components/AuthProvider";
1415
1516const ONBOARDING_KEY = ( userId : string ) => `closednote_onboarded_${ userId } ` ;
1617
18+ type SortKey = "updated" | "created" | "alpha" ;
19+
1720function WelcomeBanner ( { userName, onDismiss } : { userName : string ; onDismiss : ( ) => void } ) {
1821 return (
1922 < div className = "max-w-xl mx-auto py-16 px-4" >
@@ -28,39 +31,21 @@ function WelcomeBanner({ userName, onDismiss }: { userName: string; onDismiss: (
2831 </ p >
2932
3033 < 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 >
34+ { [
35+ [ "Save a prompt" , "Paste or type any prompt, give it a title, and hit save. Takes 10 seconds." ] ,
36+ [ "Edit freely, versions save automatically" , "Every time you update a prompt, the previous version is kept. Restore any version in one click." ] ,
37+ [ "Refine with AI when you need it" , "Open any prompt and ask AI to improve it. Add your OpenAI key in Settings to unlock GPT-4o." ] ,
38+ ] . map ( ( [ title , desc ] , i ) => (
39+ < li key = { i } className = "flex gap-4" >
40+ < 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" >
41+ { i + 1 }
42+ </ span >
43+ < div >
44+ < p className = "text-sm font-medium text-neutral-900 dark:text-neutral-100" > { title } </ p >
45+ < p className = "text-sm text-neutral-500 dark:text-neutral-400 mt-0.5" > { desc } </ p >
46+ </ div >
47+ </ li >
48+ ) ) }
6449 </ ol >
6550
6651 < div className = "flex items-center gap-4" >
@@ -82,39 +67,35 @@ function WelcomeBanner({ userName, onDismiss }: { userName: string; onDismiss: (
8267 ) ;
8368}
8469
70+ function sortPrompts ( prompts : Prompt [ ] , sort : SortKey ) : Prompt [ ] {
71+ return [ ...prompts ] . sort ( ( a , b ) => {
72+ if ( sort === "alpha" ) return a . title . localeCompare ( b . title ) ;
73+ if ( sort === "created" ) return new Date ( b . createdAt ) . getTime ( ) - new Date ( a . createdAt ) . getTime ( ) ;
74+ return new Date ( b . updatedAt ) . getTime ( ) - new Date ( a . updatedAt ) . getTime ( ) ;
75+ } ) ;
76+ }
77+
8578function DashboardContent ( ) {
8679 const { prompts : allPrompts , loading, error } = usePrompts ( ) ;
8780 const { user, loading : authLoading } = useAuth ( ) ;
88- const [ searchQuery , setSearchQuery ] = useState ( "" ) ;
89- const [ filters , setFilters ] = useState < {
90- query : string ;
91- model : PromptModel | "" ;
92- } > ( { query : "" , model : "" } ) ;
93- const [ activeCollection , setActiveCollection ] = useState <
94- string | undefined
95- > ( ) ;
81+ const searchParams = useSearchParams ( ) ;
82+ const router = useRouter ( ) ;
83+
84+ const activeCollection = searchParams . get ( "collection" ) ?? undefined ;
85+
86+ const [ filters , setFilters ] = useState < { query : string ; model : PromptModel | "" } > ( { query : "" , model : "" } ) ;
87+ const [ sort , setSort ] = useState < SortKey > ( "updated" ) ;
9688 const [ showWelcome , setShowWelcome ] = useState ( false ) ;
9789
98- useEffect ( ( ) => {
99- if ( typeof window !== "undefined" ) {
100- const urlParams = new URLSearchParams ( window . location . search ) ;
101- const collection = urlParams . get ( "collection" ) ;
102- if ( collection ) setActiveCollection ( collection ) ;
103- }
90+ const setSearchQuery = useCallback ( ( q : string ) => {
91+ setFilters ( ( prev ) => ( { ...prev , query : q } ) ) ;
10492 } , [ ] ) ;
10593
106- useEffect ( ( ) => {
107- setFilters ( ( prev ) => ( { ...prev , query : searchQuery } ) ) ;
108- } , [ searchQuery ] ) ;
109-
110- // Show welcome only once per account, on first login with no prompts
11194 useEffect ( ( ) => {
11295 if ( ! user || authLoading || loading ) return ;
11396 if ( allPrompts . length > 0 ) return ;
11497 const key = ONBOARDING_KEY ( user . id ) ;
115- if ( ! localStorage . getItem ( key ) ) {
116- setShowWelcome ( true ) ;
117- }
98+ if ( ! localStorage . getItem ( key ) ) setShowWelcome ( true ) ;
11899 } , [ user , authLoading , loading , allPrompts . length ] ) ;
119100
120101 function dismissWelcome ( ) {
@@ -130,22 +111,17 @@ function DashboardContent() {
130111 } ) ;
131112 } , [ filters , allPrompts , activeCollection ] ) ;
132113
133- const promptsByCollection = useMemo ( ( ) => {
134- return groupPromptsByCollection ( filteredPrompts ) ;
135- } , [ filteredPrompts ] ) ;
114+ const sortedPrompts = useMemo ( ( ) => sortPrompts ( filteredPrompts , sort ) , [ filteredPrompts , sort ] ) ;
136115
116+ const promptsByCollection = useMemo ( ( ) => groupPromptsByCollection ( sortedPrompts ) , [ sortedPrompts ] ) ;
137117 const collections = Object . keys ( promptsByCollection ) . sort ( ) ;
138118
119+ const isFiltering = ! ! filters . query || ! ! activeCollection ;
120+
139121 return (
140122 < Layout
141- header = {
142- < Header onSearch = { setSearchQuery } promptCount = { allPrompts . length } />
143- }
144- sidebar = {
145- allPrompts . length > 0 ? (
146- < Sidebar prompts = { allPrompts } activeCollection = { activeCollection } />
147- ) : null
148- }
123+ header = { < Header onSearch = { setSearchQuery } promptCount = { allPrompts . length } /> }
124+ sidebar = { allPrompts . length > 0 ? < Sidebar prompts = { allPrompts } activeCollection = { activeCollection } /> : null }
149125 >
150126 { error ? (
151127 < div className = "max-w-2xl mx-auto" >
@@ -155,22 +131,19 @@ function DashboardContent() {
155131 </ div >
156132 </ div >
157133 ) : ( authLoading || loading ) ? (
158- < div className = "max-w-5xl mx-auto w-full animate-pulse space-y-6 " >
159- { [ 1 , 2 , 3 ] . map ( ( i ) => (
160- < div key = { i } >
161- < div className = "h-3 bg-neutral-200 dark:bg-neutral-800 rounded w-20 mb-2 mx-3 " />
162- { [ 1 , 2 , 3 ] . map ( ( j ) => (
163- < div key = { j } className = "h-10 bg-neutral-100 dark:bg-neutral-900 rounded-md mb-1 mx-1 " />
134+ < div className = "max-w-4xl mx-auto w-full space-y-px animate-pulse " >
135+ { [ 3 , 5 , 2 ] . map ( ( n , i ) => (
136+ < div key = { i } className = "mb-4" >
137+ < div className = "h-8 bg-neutral-100 dark:bg-neutral-800/60 rounded-md mb-px " />
138+ { Array . from ( { length : n } ) . map ( ( _ , j ) => (
139+ < div key = { j } className = "h-11 bg-neutral-50 dark:bg-neutral-900 border-b border-neutral-100 dark:border-neutral-800/40 last:border-0 " />
164140 ) ) }
165141 </ div >
166142 ) ) }
167143 </ div >
168144 ) : allPrompts . length === 0 ? (
169145 showWelcome ? (
170- < WelcomeBanner
171- userName = { user ?. displayName ?. split ( " " ) [ 0 ] ?? "" }
172- onDismiss = { dismissWelcome }
173- />
146+ < WelcomeBanner userName = { user ?. displayName ?. split ( " " ) [ 0 ] ?? "" } onDismiss = { dismissWelcome } />
174147 ) : (
175148 < div className = "max-w-sm mx-auto text-center py-20" >
176149 < p className = "text-neutral-900 dark:text-neutral-100 font-medium mb-2" > No prompts yet</ p >
@@ -186,26 +159,90 @@ function DashboardContent() {
186159 </ div >
187160 )
188161 ) : (
189- < div className = "max-w-5xl mx-auto w-full" >
190- < div >
191- { collections . length === 0 ? (
192- < div className = "text-center py-16" >
193- < p className = "text-neutral-500 dark:text-neutral-400" >
194- No prompts found matching your filters.
195- </ p >
196- </ div >
197- ) : (
198- < div className = "border border-neutral-100 dark:border-neutral-800 rounded-lg overflow-hidden" >
199- { collections . map ( ( collection ) => (
200- < PromptCollection
201- key = { collection }
202- collection = { collection }
203- prompts = { promptsByCollection [ collection ] }
204- />
162+ < div className = "max-w-4xl mx-auto w-full" >
163+ { /* Toolbar */ }
164+ < div className = "flex items-center justify-between mb-3 gap-3 flex-wrap" >
165+ < div className = "flex items-center gap-2 min-w-0" >
166+ { activeCollection ? (
167+ < div className = "flex items-center gap-2" >
168+ < button
169+ onClick = { ( ) => router . push ( "/dashboard" ) }
170+ className = "text-xs text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 transition-colors"
171+ >
172+ All
173+ </ button >
174+ < span className = "text-xs text-neutral-300 dark:text-neutral-600" > /</ span >
175+ < span className = "text-xs font-medium text-neutral-700 dark:text-neutral-300 capitalize" >
176+ { activeCollection }
177+ </ span >
178+ </ div >
179+ ) : (
180+ < span className = "text-xs text-neutral-400 dark:text-neutral-500" >
181+ All prompts
182+ </ span >
183+ ) }
184+ < span className = "text-xs text-neutral-300 dark:text-neutral-700" > ·</ span >
185+ < span className = "text-xs text-neutral-400 dark:text-neutral-500 tabular-nums" >
186+ { filteredPrompts . length } { filteredPrompts . length === 1 ? "prompt" : "prompts" }
187+ { isFiltering && allPrompts . length !== filteredPrompts . length && (
188+ < span className = "text-neutral-300 dark:text-neutral-600" > of { allPrompts . length } </ span >
189+ ) }
190+ </ span >
191+ </ div >
192+
193+ < div className = "flex items-center gap-1.5" >
194+ { isFiltering && (
195+ < button
196+ onClick = { ( ) => { setFilters ( { query : "" , model : "" } ) ; router . push ( "/dashboard" ) ; } }
197+ className = "text-xs px-2.5 py-1.5 rounded-md border border-neutral-200 dark:border-neutral-700 text-neutral-500 dark:text-neutral-400 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors"
198+ >
199+ Clear filters
200+ </ button >
201+ ) }
202+ < div className = "flex items-center gap-1 bg-neutral-100 dark:bg-neutral-800 rounded-lg p-0.5" >
203+ { ( [ [ "updated" , "Recent" ] , [ "created" , "Oldest" ] , [ "alpha" , "A-Z" ] ] as [ SortKey , string ] [ ] ) . map ( ( [ key , label ] ) => (
204+ < button
205+ key = { key }
206+ onClick = { ( ) => setSort ( key ) }
207+ className = { `px-2.5 py-1 text-xs rounded-md transition-colors ${
208+ sort === key
209+ ? "bg-white dark:bg-neutral-700 text-neutral-900 dark:text-neutral-100 shadow-sm font-medium"
210+ : "text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200"
211+ } `}
212+ >
213+ { label }
214+ </ button >
205215 ) ) }
206216 </ div >
207- ) }
217+ </ div >
208218 </ div >
219+
220+ { /* Prompt list */ }
221+ { collections . length === 0 ? (
222+ < div className = "text-center py-16" >
223+ < p className = "text-sm text-neutral-500 dark:text-neutral-400" > No prompts match your search.</ p >
224+ { isFiltering && (
225+ < button
226+ onClick = { ( ) => { setFilters ( { query : "" , model : "" } ) ; router . push ( "/dashboard" ) ; } }
227+ className = "mt-3 text-sm text-neutral-900 dark:text-neutral-100 underline underline-offset-2"
228+ >
229+ Clear filters
230+ </ button >
231+ ) }
232+ </ div >
233+ ) : (
234+ < div className = "border border-neutral-200 dark:border-neutral-800 rounded-xl overflow-hidden" >
235+ { collections . map ( ( collection , i ) => (
236+ < PromptCollection
237+ key = { collection }
238+ collection = { collection }
239+ prompts = { promptsByCollection [ collection ] }
240+ defaultOpen = { collections . length <= 5 || ! ! activeCollection }
241+ bordered = { i < collections . length - 1 }
242+ />
243+ ) ) }
244+ </ div >
245+ ) }
209246 </ div >
210247 ) }
211248 </ Layout >
0 commit comments