@@ -3,8 +3,9 @@ import { Link } from "react-router-dom";
33import { Titled } from "react-titled" ;
44import { Card } from "primereact/card" ;
55import { Avatar } from "primereact/avatar" ;
6+ import { Dropdown } from "primereact/dropdown" ;
7+ import { InputSwitch } from "primereact/inputswitch" ;
68import { ProgressSpinner } from "primereact/progressspinner" ;
7- import { Message } from "primereact/message" ;
89import { Paginator } from "primereact/paginator" ;
910import gql from "graphql-tag" ;
1011import { gqlFetch } from "../graphql_fetch" ;
@@ -13,18 +14,18 @@ import { formatDistanceToNow } from "date-fns";
1314import { generateRetroAvatar } from "../lib/avatar" ;
1415
1516const GET_PUBLIC_PROFILES = gql `
16- query GetPublicProfiles($limit: Int!, $offset: Int!) {
17- user_aggregate(where: { profile_is_public: { _eq: true } }) {
17+ query GetPublicProfiles(
18+ $where: user_bool_exp!
19+ $orderBy: [user_order_by!]!
20+ $limit: Int!
21+ $offset: Int!
22+ ) {
23+ user_aggregate(where: $where) {
1824 aggregate {
1925 count
2026 }
2127 }
22- user(
23- where: { profile_is_public: { _eq: true } }
24- order_by: { created_at: desc }
25- limit: $limit
26- offset: $offset
27- ) {
28+ user(where: $where, order_by: $orderBy, limit: $limit, offset: $offset) {
2829 user_id
2930 greeting_name
3031 slug
@@ -37,11 +38,48 @@ const GET_PUBLIC_PROFILES = gql`
3738 count
3839 }
3940 }
41+ following_aggregate {
42+ aggregate {
43+ count
44+ }
45+ }
46+ projects_aggregate(where: { is_public: { _eq: true } }) {
47+ aggregate {
48+ count
49+ }
50+ }
4051 }
4152 }
4253` ;
4354
55+ const SORT_OPTIONS = [
56+ {
57+ label : "Recent activity" ,
58+ value : "recent" ,
59+ orderBy : [ { projects_aggregate : { max : { updated_at : "desc_nulls_last" } } } ] ,
60+ } ,
61+ {
62+ label : "Most public projects" ,
63+ value : "projects" ,
64+ orderBy : [ { projects_aggregate : { count : "desc" } } ] ,
65+ } ,
66+ {
67+ label : "Most followers" ,
68+ value : "followers" ,
69+ orderBy : [ { followers_aggregate : { count : "desc" } } ] ,
70+ } ,
71+ {
72+ label : "Newest" ,
73+ value : "newest" ,
74+ orderBy : [ { created_at : "desc" } ] ,
75+ } ,
76+ ] ;
77+
4478function ProfileCard ( { user } ) {
79+ const projectCount = user . projects_aggregate ?. aggregate ?. count || 0 ;
80+ const followerCount = user . followers_aggregate ?. aggregate ?. count || 0 ;
81+ const followingCount = user . following_aggregate ?. aggregate ?. count || 0 ;
82+
4583 return (
4684 < Link to = { `/u/${ user . slug || user . user_id } ` } className = "no-underline" >
4785 < Card
@@ -61,15 +99,19 @@ function ProfileCard({ user }) {
6199 } }
62100 />
63101 < div className = "flex-1" >
64- < h4 className = "m-0 mb-1 text-white" > { user . greeting_name } </ h4 >
102+ < h4 className = "m-0 mb-1 text-white" >
103+ { user . greeting_name || `@${ user . slug } ` }
104+ </ h4 >
65105 < p className = "text-500 m-0 text-sm" > @{ user . slug } </ p >
66106 { user . bio && (
67107 < p className = "text-400 m-0 mt-2 text-sm" > { user . bio } </ p >
68108 ) }
69109 < div className = "text-400 text-sm mt-2" >
70- < span >
71- { user . followers_aggregate ?. aggregate ?. count || 0 } followers
72- </ span >
110+ < span > { projectCount } public projects</ span >
111+ < span className = "mx-2" > ·</ span >
112+ < span > { followerCount } followers</ span >
113+ < span className = "mx-2" > ·</ span >
114+ < span > { followingCount } following</ span >
73115 < span className = "mx-2" > ·</ span >
74116 < span >
75117 Joined{ " " }
@@ -89,21 +131,33 @@ export default function PublicProfiles() {
89131 const [ profiles , setProfiles ] = useState ( [ ] ) ;
90132 const [ totalCount , setTotalCount ] = useState ( 0 ) ;
91133 const [ page , setPage ] = useState ( 0 ) ;
134+ const [ sort , setSort ] = useState ( "recent" ) ;
135+ const [ hideEmpty , setHideEmpty ] = useState ( true ) ;
92136 const [ loading , setLoading ] = useState ( true ) ;
93137 const pageSize = 20 ;
94138
95139 useEffect ( ( ) => {
96140 const fetchProfiles = async ( ) => {
97141 try {
98142 setLoading ( true ) ;
143+
144+ const where = { profile_is_public : { _eq : true } } ;
145+ if ( hideEmpty ) {
146+ where . projects = { is_public : { _eq : true } } ;
147+ }
148+
149+ const orderBy =
150+ SORT_OPTIONS . find ( ( o ) => o . value === sort ) ?. orderBy ||
151+ SORT_OPTIONS [ 0 ] . orderBy ;
152+
99153 const response = await gqlFetch ( null , GET_PUBLIC_PROFILES , {
154+ where,
155+ orderBy,
100156 limit : pageSize ,
101157 offset : page * pageSize ,
102158 } ) ;
103159 setProfiles ( response ?. data ?. user || [ ] ) ;
104- setTotalCount (
105- response ?. data ?. user_aggregate ?. aggregate ?. count || 0
106- ) ;
160+ setTotalCount ( response ?. data ?. user_aggregate ?. aggregate ?. count || 0 ) ;
107161 } catch ( err ) {
108162 console . error ( "Failed to fetch public profiles:" , err ) ;
109163 } finally {
@@ -112,31 +166,52 @@ export default function PublicProfiles() {
112166 } ;
113167
114168 fetchProfiles ( ) ;
115- } , [ page ] ) ;
116-
117- if ( loading ) {
118- return (
119- < div className = "flex justify-content-center align-items-center spinner-container-centered" >
120- < ProgressSpinner />
121- </ div >
122- ) ;
123- }
169+ } , [ page , sort , hideEmpty ] ) ;
124170
125171 return (
126172 < Titled title = { ( s ) => `Public Profiles ${ sep } ${ s } ` } >
127173 < div className = "m-2" >
128174 < Card title = "Public Profiles" >
129- < p className = "text-500 mb-4" >
130- Browse public profiles on ZX Play
131- { totalCount > 0 && (
132- < span className = "ml-2" >
133- (showing { page * pageSize + 1 } -{ " " }
134- { Math . min ( ( page + 1 ) * pageSize , totalCount ) } of { totalCount } )
135- </ span >
136- ) }
137- </ p >
175+ < div className = "flex flex-wrap align-items-center justify-content-between gap-3 mb-4" >
176+ < p className = "text-500 m-0" >
177+ Browse public profiles on ZX Play
178+ { totalCount > 0 && (
179+ < span className = "ml-2" >
180+ (showing { page * pageSize + 1 } -{ " " }
181+ { Math . min ( ( page + 1 ) * pageSize , totalCount ) } of { totalCount } )
182+ </ span >
183+ ) }
184+ </ p >
185+ < div className = "flex align-items-center gap-3" >
186+ < div className = "flex align-items-center gap-2" >
187+ < InputSwitch
188+ inputId = "hideEmpty"
189+ checked = { hideEmpty }
190+ onChange = { ( e ) => {
191+ setHideEmpty ( e . value ) ;
192+ setPage ( 0 ) ;
193+ } }
194+ />
195+ < label htmlFor = "hideEmpty" className = "text-500 text-sm" >
196+ Only with public projects
197+ </ label >
198+ </ div >
199+ < Dropdown
200+ value = { sort }
201+ options = { SORT_OPTIONS }
202+ onChange = { ( e ) => {
203+ setSort ( e . value ) ;
204+ setPage ( 0 ) ;
205+ } }
206+ />
207+ </ div >
208+ </ div >
138209
139- { profiles . length > 0 ? (
210+ { loading ? (
211+ < div className = "flex justify-content-center align-items-center py-6" >
212+ < ProgressSpinner />
213+ </ div >
214+ ) : profiles . length > 0 ? (
140215 < >
141216 < div className = "grid" >
142217 { profiles . map ( ( user ) => (
0 commit comments