Skip to content

Commit 96c972a

Browse files
committed
Public profiles list
1 parent 6ce2588 commit 96c972a

3 files changed

Lines changed: 180 additions & 0 deletions

File tree

apps/web/src/components/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import UserProfileSettings from "./UserProfileSettings";
2424
import YourProjectsPage from "./YourProjectsPage";
2525
import ActivityFeed from "./ActivityFeed";
2626
import FollowList from "./FollowList";
27+
import PublicProfiles from "./PublicProfiles";
2728
import ErrorNotFoundPage from "./ErrorNotFoundPage";
2829
import ErrorPage from "./ErrorPage";
2930
import clsx from "clsx";
@@ -150,6 +151,7 @@ export default function App() {
150151
<Route exact path="/u/:slug/followers" element={<FollowList />} />
151152
<Route exact path="/u/:slug/following" element={<FollowList />} />
152153
<Route exact path="/feed" element={<ActivityFeed />} />
154+
<Route exact path="/profiles" element={<PublicProfiles />} />
153155
<Route
154156
exact
155157
path="/settings/profile"

apps/web/src/components/Nav.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,14 @@ function getMenuItems(navigate, userId, userSlug, dispatch, lang, emuVisible) {
218218
},
219219
};
220220

221+
const publicProfilesMenuItem = {
222+
label: "Public Profiles",
223+
icon: "pi pi-fw pi-users",
224+
command: () => {
225+
navigate(`/profiles`);
226+
},
227+
};
228+
221229
const viewMenu = {
222230
label: "View",
223231
icon: "pi pi-fw pi-eye",
@@ -227,6 +235,7 @@ function getMenuItems(navigate, userId, userSlug, dispatch, lang, emuVisible) {
227235
viewMenu.items.push(viewFullScreenMenuItem);
228236
viewMenu.items.push(sep);
229237
viewMenu.items.push(feedMenuItem);
238+
viewMenu.items.push(publicProfilesMenuItem);
230239
viewMenu.items.push(viewProfileMenuItem);
231240
viewMenu.items.push(profileSettingsMenuItem);
232241

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import React, { useEffect, useState } from "react";
2+
import { Link } from "react-router-dom";
3+
import { Titled } from "react-titled";
4+
import { Card } from "primereact/card";
5+
import { Avatar } from "primereact/avatar";
6+
import { ProgressSpinner } from "primereact/progressspinner";
7+
import { Message } from "primereact/message";
8+
import { Paginator } from "primereact/paginator";
9+
import gql from "graphql-tag";
10+
import { gqlFetch } from "../graphql_fetch";
11+
import { sep } from "../constants";
12+
import { formatDistanceToNow } from "date-fns";
13+
import { generateRetroAvatar } from "../lib/avatar";
14+
15+
const GET_PUBLIC_PROFILES = gql`
16+
query GetPublicProfiles($limit: Int!, $offset: Int!) {
17+
user_aggregate(where: { profile_is_public: { _eq: true } }) {
18+
aggregate {
19+
count
20+
}
21+
}
22+
user(
23+
where: { profile_is_public: { _eq: true } }
24+
order_by: { created_at: desc }
25+
limit: $limit
26+
offset: $offset
27+
) {
28+
user_id
29+
greeting_name
30+
slug
31+
bio
32+
created_at
33+
avatar_variant
34+
custom_avatar_data
35+
followers_aggregate {
36+
aggregate {
37+
count
38+
}
39+
}
40+
}
41+
}
42+
`;
43+
44+
function ProfileCard({ user }) {
45+
return (
46+
<Link to={`/u/${user.slug || user.user_id}`} className="no-underline">
47+
<Card
48+
className="h-full hover:shadow-5 transition-all transition-duration-200 cursor-pointer card-bg-dark"
49+
style={{ border: "none" }}
50+
>
51+
<div className="flex align-items-center">
52+
<Avatar
53+
image={generateRetroAvatar(user.slug || user.user_id, 64, user)}
54+
size="large"
55+
shape="square"
56+
className="mr-3"
57+
style={{
58+
width: "64px",
59+
height: "64px",
60+
imageRendering: "pixelated",
61+
}}
62+
/>
63+
<div className="flex-1">
64+
<h4 className="m-0 mb-1 text-white">{user.greeting_name}</h4>
65+
<p className="text-500 m-0 text-sm">@{user.slug}</p>
66+
{user.bio && (
67+
<p className="text-400 m-0 mt-2 text-sm">{user.bio}</p>
68+
)}
69+
<div className="text-400 text-sm mt-2">
70+
<span>
71+
{user.followers_aggregate?.aggregate?.count || 0} followers
72+
</span>
73+
<span className="mx-2">·</span>
74+
<span>
75+
Joined{" "}
76+
{formatDistanceToNow(new Date(user.created_at), {
77+
addSuffix: true,
78+
})}
79+
</span>
80+
</div>
81+
</div>
82+
</div>
83+
</Card>
84+
</Link>
85+
);
86+
}
87+
88+
export default function PublicProfiles() {
89+
const [profiles, setProfiles] = useState([]);
90+
const [totalCount, setTotalCount] = useState(0);
91+
const [page, setPage] = useState(0);
92+
const [loading, setLoading] = useState(true);
93+
const pageSize = 20;
94+
95+
useEffect(() => {
96+
const fetchProfiles = async () => {
97+
try {
98+
setLoading(true);
99+
const response = await gqlFetch(null, GET_PUBLIC_PROFILES, {
100+
limit: pageSize,
101+
offset: page * pageSize,
102+
});
103+
setProfiles(response?.data?.user || []);
104+
setTotalCount(
105+
response?.data?.user_aggregate?.aggregate?.count || 0
106+
);
107+
} catch (err) {
108+
console.error("Failed to fetch public profiles:", err);
109+
} finally {
110+
setLoading(false);
111+
}
112+
};
113+
114+
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+
}
124+
125+
return (
126+
<Titled title={(s) => `Public Profiles ${sep} ${s}`}>
127+
<div className="m-2">
128+
<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>
138+
139+
{profiles.length > 0 ? (
140+
<>
141+
<div className="grid">
142+
{profiles.map((user) => (
143+
<div key={user.user_id} className="col-12 md:col-6">
144+
<ProfileCard user={user} />
145+
</div>
146+
))}
147+
</div>
148+
149+
{totalCount > pageSize && (
150+
<Paginator
151+
first={page * pageSize}
152+
rows={pageSize}
153+
totalRecords={totalCount}
154+
onPageChange={(e) => setPage(e.page)}
155+
className="mt-3"
156+
/>
157+
)}
158+
</>
159+
) : (
160+
<div className="text-center py-4">
161+
<i className="pi pi-users text-4xl text-300 mb-3" />
162+
<p className="text-500">No public profiles yet</p>
163+
</div>
164+
)}
165+
</Card>
166+
</div>
167+
</Titled>
168+
);
169+
}

0 commit comments

Comments
 (0)