33import type { Project } from "@/api/projects" ;
44import type { Team } from "@/api/team" ;
55import { ProjectAvatar } from "@/components/blocks/Avatars/ProjectAvatar" ;
6- import { CopyButton } from "@/components/ui/CopyButton " ;
6+ import { CopyTextButton } from "@/components/ui/CopyTextButton " ;
77import { Button } from "@/components/ui/button" ;
88import {
99 DropdownMenu ,
1010 DropdownMenuContent ,
1111 DropdownMenuTrigger ,
1212} from "@/components/ui/dropdown-menu" ;
1313import { Input } from "@/components/ui/input" ;
14+ import {
15+ Popover ,
16+ PopoverContent ,
17+ PopoverTrigger ,
18+ } from "@/components/ui/popover" ;
1419import {
1520 Select ,
1621 SelectContent ,
@@ -19,43 +24,58 @@ import {
1924} from "@/components/ui/select" ;
2025import { useDashboardRouter } from "@/lib/DashboardRouter" ;
2126import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog" ;
22- import { ChevronDownIcon , PlusIcon , SearchIcon } from "lucide-react" ;
27+ import {
28+ ChevronDownIcon ,
29+ EllipsisVerticalIcon ,
30+ PlusIcon ,
31+ SearchIcon ,
32+ } from "lucide-react" ;
2333import Link from "next/link" ;
24- import { useState } from "react" ;
34+ import { useMemo , useState } from "react" ;
35+
36+ type SortById = "name" | "createdAt" | "totalConnections" ;
2537
26- type SortById = "name" | "createdAt" ;
38+ type ProjectWithTotalConnections = Project & { totalConnections : number } ;
2739
2840export function TeamProjectsPage ( props : {
29- projects : Project [ ] ;
41+ projects : ProjectWithTotalConnections [ ] ;
3042 team : Team ;
3143} ) {
3244 const { projects } = props ;
3345 const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
34- const [ sortBy , setSortBy ] = useState < SortById > ( "createdAt " ) ;
46+ const [ sortBy , setSortBy ] = useState < SortById > ( "totalConnections " ) ;
3547 const [ isCreateProjectDialogOpen , setIsCreateProjectDialogOpen ] =
3648 useState ( false ) ;
3749 const router = useDashboardRouter ( ) ;
3850
39- let projectsToShow = ! searchTerm
40- ? projects
41- : projects . filter (
42- ( project ) =>
43- project . name . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
44- project . publishableKey
45- . toLowerCase ( )
46- . includes ( searchTerm . toLowerCase ( ) ) ,
51+ const projectsToShow = useMemo ( ( ) => {
52+ let _projectsToShow = ! searchTerm
53+ ? projects
54+ : projects . filter (
55+ ( project ) =>
56+ project . name . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
57+ project . publishableKey
58+ . toLowerCase ( )
59+ . includes ( searchTerm . toLowerCase ( ) ) ,
60+ ) ;
61+
62+ if ( sortBy === "name" ) {
63+ _projectsToShow = _projectsToShow . sort ( ( a , b ) =>
64+ a . name . localeCompare ( b . name ) ,
65+ ) ;
66+ } else if ( sortBy === "createdAt" ) {
67+ _projectsToShow = _projectsToShow . sort (
68+ ( a , b ) =>
69+ new Date ( b . createdAt ) . getTime ( ) - new Date ( a . createdAt ) . getTime ( ) ,
70+ ) ;
71+ } else if ( sortBy === "totalConnections" ) {
72+ _projectsToShow = _projectsToShow . sort (
73+ ( a , b ) => b . totalConnections - a . totalConnections ,
4774 ) ;
75+ }
4876
49- if ( sortBy === "name" ) {
50- projectsToShow = projectsToShow . sort ( ( a , b ) =>
51- a . name . localeCompare ( b . name ) ,
52- ) ;
53- } else if ( sortBy === "createdAt" ) {
54- projectsToShow = projectsToShow . sort (
55- ( a , b ) =>
56- new Date ( b . createdAt ) . getTime ( ) - new Date ( a . createdAt ) . getTime ( ) ,
57- ) ;
58- }
77+ return _projectsToShow ;
78+ } , [ searchTerm , sortBy , projects ] ) ;
5979
6080 return (
6181 < div className = "flex grow flex-col" >
@@ -75,19 +95,29 @@ export function TeamProjectsPage(props: {
7595
7696 { /* Projects */ }
7797 { projectsToShow . length === 0 ? (
78- < div className = "flex min-h-[450px] grow items-center justify-center rounded-lg border border-border" >
79- < div className = "flex flex-col items-center" >
80- < p className = "mb-5 text-center" > No projects created</ p >
81- < Button
82- className = "gap-2"
83- onClick = { ( ) => setIsCreateProjectDialogOpen ( true ) }
84- variant = "outline"
85- >
86- < PlusIcon className = "size-4" />
87- Create a Project
88- </ Button >
89- </ div >
90- </ div >
98+ < >
99+ { searchTerm !== "" ? (
100+ < div className = "flex min-h-[450px] grow items-center justify-center rounded-lg border border-border" >
101+ < div className = "flex flex-col items-center" >
102+ < p className = "mb-5 text-center" > No projects found</ p >
103+ </ div >
104+ </ div >
105+ ) : (
106+ < div className = "flex min-h-[450px] grow items-center justify-center rounded-lg border border-border" >
107+ < div className = "flex flex-col items-center" >
108+ < p className = "mb-5 text-center" > No projects created</ p >
109+ < Button
110+ className = "gap-2"
111+ onClick = { ( ) => setIsCreateProjectDialogOpen ( true ) }
112+ variant = "outline"
113+ >
114+ < PlusIcon className = "size-4" />
115+ Create a Project
116+ </ Button >
117+ </ div >
118+ </ div >
119+ ) }
120+ </ >
91121 ) : (
92122 < div className = "grid grid-cols-1 gap-5 md:grid-cols-2" >
93123 { projectsToShow . map ( ( project ) => {
@@ -118,7 +148,7 @@ export function TeamProjectsPage(props: {
118148}
119149
120150function ProjectCard ( props : {
121- project : Project ;
151+ project : ProjectWithTotalConnections ;
122152 team_slug : string ;
123153} ) {
124154 const { project, team_slug } = props ;
@@ -130,34 +160,51 @@ function ProjectCard(props: {
130160 { /* TODO - set image */ }
131161 < ProjectAvatar className = "size-10 rounded-full" src = "" />
132162
133- < div className = "flex-grow flex-col gap-1" >
134- < div className = "flex items-center justify-between gap-2" >
135- < Link
136- className = "group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
137- // remove /connect when we have overview page
138- href = { `/team/${ team_slug } /${ project . slug } ` }
139- >
140- < h2 className = "font-medium text-base" > { project . name } </ h2 >
141- </ Link >
142- < CopyButton
143- text = { project . publishableKey }
144- iconClassName = "z-10 size-3"
145- className = "!h-auto !w-auto -translate-x-1 p-2 hover:bg-secondary"
146- />
147- </ div >
163+ < div className = "flex-grow flex-col gap-1.5" >
164+ < Link
165+ className = "group static before:absolute before:top-0 before:right-0 before:bottom-0 before:left-0 before:z-0"
166+ // remove /connect when we have overview page
167+ href = { `/team/${ team_slug } /${ project . slug } ` }
168+ >
169+ < h2 className = "font-medium text-base" > { project . name } </ h2 >
170+ </ Link >
148171
149- < p className = "flex items-center text-muted-foreground text-sm" >
150- { truncate ( project . publishableKey , 32 ) }
172+ < p className = "flex items-center gap-1 text-muted-foreground text-sm" >
173+ < span > { project . totalConnections } </ span >
174+ Total Users
151175 </ p >
152176 </ div >
177+
178+ < Popover >
179+ < PopoverTrigger asChild >
180+ < Button className = "z-10 h-auto w-auto p-2" variant = "ghost" >
181+ < EllipsisVerticalIcon className = "size-4" />
182+ </ Button >
183+ </ PopoverTrigger >
184+ < PopoverContent className = "w-[180px] p-1" >
185+ < CopyTextButton
186+ textToCopy = { project . publishableKey }
187+ textToShow = "Copy Client ID"
188+ copyIconPosition = "right"
189+ tooltip = { undefined }
190+ variant = "ghost"
191+ className = "flex h-10 w-full justify-between gap-3 rounded-md px-4 py-2"
192+ />
193+ < Button
194+ variant = "ghost"
195+ className = "w-full justify-start gap-3"
196+ asChild
197+ >
198+ < Link href = { `/team/${ team_slug } /${ project . slug } /settings` } >
199+ Settings
200+ </ Link >
201+ </ Button >
202+ </ PopoverContent >
203+ </ Popover >
153204 </ div >
154205 ) ;
155206}
156207
157- function truncate ( str : string , stringLimit : number ) {
158- return str . length > stringLimit ? `${ str . slice ( 0 , stringLimit ) } ...` : str ;
159- }
160-
161208function SearchInput ( props : {
162209 value : string ;
163210 onValueChange : ( value : string ) => void ;
@@ -209,10 +256,11 @@ function SelectBy(props: {
209256 value : SortById ;
210257 onChange : ( value : SortById ) => void ;
211258} ) {
212- const values : SortById [ ] = [ "name" , "createdAt" ] ;
259+ const values : SortById [ ] = [ "name" , "createdAt" , "totalConnections" ] ;
213260 const valueToLabel : Record < SortById , string > = {
214261 name : "Name" ,
215262 createdAt : "Creation Date" ,
263+ totalConnections : "Total Users" ,
216264 } ;
217265
218266 return (
@@ -223,7 +271,12 @@ function SelectBy(props: {
223271 } }
224272 >
225273 < SelectTrigger className = "min-w-[200px] bg-card capitalize" >
226- Sort by { valueToLabel [ props . value ] }
274+ < div className = "flex items-center gap-1.5" >
275+ < span className = "!hidden lg:!inline text-muted-foreground" >
276+ Sort by
277+ </ span >
278+ { valueToLabel [ props . value ] }
279+ </ div >
227280 </ SelectTrigger >
228281 < SelectContent >
229282 { values . map ( ( value ) => (
0 commit comments