@@ -38,6 +38,10 @@ const Dashboard: React.FC = () => {
3838 const [ roleFilter , setRoleFilter ] = useState < WorkspaceRole | "all" > ( "all" ) ;
3939 const [ filterOpen , setFilterOpen ] = useState ( false ) ;
4040
41+ // pagination state
42+ const ITEMS_PER_PAGE = 6 ; // 2 rows * 3 columns on desktop
43+ const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
44+
4145 useEffect ( ( ) => {
4246 if ( userId && ! initialLoadDone ) {
4347 fetchWorkspaces ( ) ;
@@ -66,6 +70,21 @@ const Dashboard: React.FC = () => {
6670 ? workspaces
6771 : workspaces . filter ( ( w ) => w . currentUserRole === roleFilter ) ;
6872
73+ // reset page when filter changes or list length changes
74+ useEffect ( ( ) => {
75+ setCurrentPage ( 1 ) ;
76+ } , [ roleFilter , workspaces . length ] ) ;
77+
78+ const totalPages = Math . max (
79+ 1 ,
80+ Math . ceil ( filteredWorkspaces . length / ITEMS_PER_PAGE )
81+ ) ;
82+ const startIndex = ( currentPage - 1 ) * ITEMS_PER_PAGE ;
83+ const currentItems = filteredWorkspaces . slice (
84+ startIndex ,
85+ startIndex + ITEMS_PER_PAGE
86+ ) ;
87+
6988 return (
7089 < div className = "min-h-screen bg-linear-to-br from-slate-50 via-white to-slate-100" >
7190 < div className = "mx-auto max-w-6xl px-4 py-8 sm:px-6 lg:px-8 space-y-8" >
@@ -110,8 +129,7 @@ const Dashboard: React.FC = () => {
110129 </ div >
111130 ) }
112131
113- { /* Stats row – all three boxes same size */ }
114- { /* Stats row – compact content, same card size */ }
132+ { /* Stats row – compact */ }
115133 < section className = "grid grid-cols-3 gap-3 sm:gap-4" >
116134 { /* Workspaces count */ }
117135 < div className = "rounded-xl border border-slate-200/50 bg-white/80 backdrop-blur-sm px-4 py-3 shadow-sm flex items-center justify-between min-h-[72px]" >
@@ -123,7 +141,7 @@ const Dashboard: React.FC = () => {
123141 </ div >
124142 </ div >
125143
126- { /* Filter card with dropdown (unchanged alignment) */ }
144+ { /* Filter card with dropdown */ }
127145 < div className = "relative" >
128146 < div
129147 className = "rounded-xl border border-slate-200/50 bg-white/80 backdrop-blur-sm px-4 py-3 shadow-sm flex items-center justify-between min-h-[72px] cursor-pointer hover:shadow-md transition-shadow"
@@ -188,7 +206,7 @@ const Dashboard: React.FC = () => {
188206 </ div >
189207 </ section >
190208
191- { /* Workspaces */ }
209+ { /* Workspaces with pagination */ }
192210 < section className = "space-y-4" >
193211 { isLoading && ! workspaces . length && (
194212 < div className = "flex flex-col items-center justify-center gap-3 rounded-2xl border-2 border-dashed border-slate-200/50 bg-white/70 backdrop-blur-sm px-8 py-12 text-center shadow-sm" >
@@ -210,56 +228,105 @@ const Dashboard: React.FC = () => {
210228 </ div >
211229 ) }
212230
213- { workspaces . length > 0 && filteredWorkspaces . length === 0 && (
231+ { filteredWorkspaces . length > 0 && currentItems . length === 0 && (
214232 < div className = "rounded-2xl border-2 border-dashed border-slate-200/50 bg-white/70 backdrop-blur-sm px-6 py-8 text-center text-sm text-slate-500 shadow-sm" >
215233 No workspaces match this filter.
216234 </ div >
217235 ) }
218236
219- { filteredWorkspaces . length > 0 && (
220- < div className = "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3" >
221- { filteredWorkspaces . map ( ( ws ) => {
222- const role = ws . currentUserRole ;
223- const color = ROLE_COLORS [ role ] || "bg-slate-400" ;
237+ { currentItems . length > 0 && (
238+ < >
239+ < div className = "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3" >
240+ { currentItems . map ( ( ws ) => {
241+ const role = ws . currentUserRole ;
242+ const color = ROLE_COLORS [ role ] || "bg-slate-400" ;
224243
225- return (
226- < div
227- key = { ws . id }
228- className = "group flex flex-col rounded-2xl border border-slate-200/50 bg-white/80 backdrop-blur-sm p-6 shadow-sm hover:shadow-xl hover:shadow-slate-300/40 hover:-translate-y-1 transition-all duration-300 hover:border-slate-300/70"
229- >
230- < div className = "flex items-start justify-between gap-4" >
231- < div className = "min-w-0" >
232- < h3 className = "truncate text-lg font-semibold text-slate-900 group-hover:text-indigo-700 transition-colors" >
233- { ws . name || "Untitled workspace" }
234- </ h3 >
235- < p className = "mt-1 text-xs text-slate-500" >
236- { new Date ( ws . createdAt ) . toLocaleDateString ( ) }
237- </ p >
244+ return (
245+ < div
246+ key = { ws . id }
247+ className = "group flex flex-col rounded-2xl border border-slate-200/50 bg-white/80 backdrop-blur-sm p-6 shadow-sm hover:shadow-xl hover:shadow-slate-300/40 hover:-translate-y-1 transition-all duration-300 hover:border-slate-300/70"
248+ >
249+ < div className = "flex items-start justify-between gap-4" >
250+ < div className = "min-w-0" >
251+ < h3 className = "truncate text-lg font-semibold text-slate-900 group-hover:text-indigo-700 transition-colors" >
252+ { ws . name || "Untitled workspace" }
253+ </ h3 >
254+ < p className = "mt-1 text-xs text-slate-500" >
255+ { new Date ( ws . createdAt ) . toLocaleDateString ( ) }
256+ </ p >
257+ </ div >
258+
259+ < span
260+ className = { `inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold text-white shadow-sm ${ color } ` }
261+ >
262+ { role }
263+ </ span >
238264 </ div >
239265
240- < span
241- className = { `inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold text-white shadow-sm ${ color } ` }
242- >
243- { role }
244- </ span >
266+ { ( role === "Owner" || role === "Admin" ) && (
267+ < div className = "mt-6 pt-4 border-t border-slate-100/50 flex items-center justify-between text-xs text-slate-500" >
268+ < button
269+ onClick = { ( ) => handleInvite ( ws . id ) }
270+ className = "font-semibold text-slate-700 hover:text-slate-900 transition-colors"
271+ aria-label = { `Invite member to ${ ws . name } ` }
272+ >
273+ Invite
274+ </ button >
275+ < span className = "text-slate-400 text-xs" >
276+ Members
277+ </ span >
278+ </ div >
279+ ) }
245280 </ div >
281+ ) ;
282+ } ) }
283+ </ div >
246284
247- { ( role === "Owner" || role === "Admin" ) && (
248- < div className = "mt-6 pt-4 border-t border-slate-100/50 flex items-center justify-between text-xs text-slate-500" >
285+ { /* Pagination controls */ }
286+ { totalPages > 1 && (
287+ < div className = "flex items-center justify-between gap-3 pt-2" >
288+ < div className = "text-xs text-slate-500" >
289+ Page { currentPage } of { totalPages }
290+ </ div >
291+ < div className = "flex items-center gap-1" >
292+ < button
293+ type = "button"
294+ onClick = { ( ) => setCurrentPage ( ( p ) => Math . max ( 1 , p - 1 ) ) }
295+ disabled = { currentPage === 1 }
296+ className = "px-2 py-1 text-xs rounded-md border border-slate-200 bg-white text-slate-700 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-50"
297+ >
298+ Prev
299+ </ button >
300+ { Array . from ( { length : totalPages } , ( _ , i ) => i + 1 ) . map (
301+ ( page ) => (
249302 < button
250- onClick = { ( ) => handleInvite ( ws . id ) }
251- className = "font-semibold text-slate-700 hover:text-slate-900 transition-colors"
252- aria-label = { `Invite member to ${ ws . name } ` }
303+ key = { page }
304+ type = "button"
305+ onClick = { ( ) => setCurrentPage ( page ) }
306+ className = { `px-2.5 py-1 text-xs rounded-md border ${
307+ currentPage === page
308+ ? "border-slate-900 bg-slate-900 text-white"
309+ : "border-slate-200 bg-white text-slate-700 hover:bg-slate-50"
310+ } `}
253311 >
254- Invite
312+ { page }
255313 </ button >
256- < span className = "text-slate-400 text-xs" > Members</ span >
257- </ div >
314+ )
258315 ) }
316+ < button
317+ type = "button"
318+ onClick = { ( ) =>
319+ setCurrentPage ( ( p ) => Math . min ( totalPages , p + 1 ) )
320+ }
321+ disabled = { currentPage === totalPages }
322+ className = "px-2 py-1 text-xs rounded-md border border-slate-200 bg-white text-slate-700 disabled:opacity-40 disabled:cursor-not-allowed hover:bg-slate-50"
323+ >
324+ Next
325+ </ button >
259326 </ div >
260- ) ;
261- } ) }
262- </ div >
327+ </ div >
328+ ) }
329+ </ >
263330 ) }
264331 </ section >
265332 </ div >
0 commit comments