From 4ea4f3149ff7216ea85372fc7dbf788322c1d710 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 9 Sep 2025 03:07:48 +0200 Subject: [PATCH] feat: add user count endpoint and integrate it into frontend components --- thoughts-backend/api/src/routers/user.rs | 6 ++ thoughts-backend/app/src/persistence/user.rs | 4 ++ thoughts-backend/tests/api/user.rs | 22 +++++++ thoughts-frontend/app/page.tsx | 2 + thoughts-frontend/app/users/all/page.tsx | 63 ++++++++++++++++++ thoughts-frontend/components/main-nav.tsx | 6 +- thoughts-frontend/components/users-count.tsx | 69 ++++++++++++++++++++ thoughts-frontend/lib/api.ts | 25 ++++++- 8 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 thoughts-frontend/app/users/all/page.tsx create mode 100644 thoughts-frontend/components/users-count.tsx diff --git a/thoughts-backend/api/src/routers/user.rs b/thoughts-backend/api/src/routers/user.rs index 7a14210..c7a45f2 100644 --- a/thoughts-backend/api/src/routers/user.rs +++ b/thoughts-backend/api/src/routers/user.rs @@ -451,10 +451,16 @@ async fn get_all_users_public( Ok(Json(response)) } +async fn get_all_users_count(State(state): State) -> Result { + let count = app::persistence::user::get_all_users_count(&state.conn).await?; + Ok(Json(json!({ "count": count }))) +} + pub fn create_user_router() -> Router { Router::new() .route("/", get(users_get)) .route("/all", get(get_all_users_public)) + .route("/count", get(get_all_users_count)) .route("/me", get(get_me).put(update_me)) .nest("/me/api-keys", create_api_key_router()) .route("/{param}", get(get_user_by_param)) diff --git a/thoughts-backend/app/src/persistence/user.rs b/thoughts-backend/app/src/persistence/user.rs index ff40b66..ab0de03 100644 --- a/thoughts-backend/app/src/persistence/user.rs +++ b/thoughts-backend/app/src/persistence/user.rs @@ -180,3 +180,7 @@ pub async fn get_all_users( Ok((users, total_items)) } + +pub async fn get_all_users_count(db: &DbConn) -> Result { + user::Entity::find().count(db).await +} diff --git a/thoughts-backend/tests/api/user.rs b/thoughts-backend/tests/api/user.rs index 7da4a10..02f0166 100644 --- a/thoughts-backend/tests/api/user.rs +++ b/thoughts-backend/tests/api/user.rs @@ -288,3 +288,25 @@ async fn test_get_all_users_paginated() { assert_eq!(v_p2["page"], 2); assert_eq!(v_p2["totalPages"], 2); } + +#[tokio::test] +async fn test_get_all_users_count() { + let app = setup().await; + + for i in 0..25 { + create_user_with_password( + &app.db, + &format!("user{}", i), + "password123", + &format!("u{}@e.com", i), + ) + .await; + } + + let response = make_get_request(app.router.clone(), "/users/count", None).await; + assert_eq!(response.status(), StatusCode::OK); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let v: Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!(v["count"], 25); +} diff --git a/thoughts-frontend/app/page.tsx b/thoughts-frontend/app/page.tsx index 495e0d3..6345c3d 100644 --- a/thoughts-frontend/app/page.tsx +++ b/thoughts-frontend/app/page.tsx @@ -14,6 +14,7 @@ import { PopularTags } from "@/components/popular-tags"; import { ThoughtThread } from "@/components/thought-thread"; import { buildThoughtThreads } from "@/lib/utils"; import { TopFriends } from "@/components/top-friends"; +import { UsersCount } from "@/components/users-count"; export default async function Home() { const token = (await cookies()).get("auth_token")?.value ?? null; @@ -92,6 +93,7 @@ async function FeedPage({ token }: { token: string }) { )} {token && } + diff --git a/thoughts-frontend/app/users/all/page.tsx b/thoughts-frontend/app/users/all/page.tsx new file mode 100644 index 0000000..077b3ca --- /dev/null +++ b/thoughts-frontend/app/users/all/page.tsx @@ -0,0 +1,63 @@ +import { getAllUsers } from "@/lib/api"; +import { UserListCard } from "@/components/user-list-card"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; + +export default async function AllUsersPage({ + searchParams, +}: { + searchParams: { page?: string }; +}) { + const page = parseInt(searchParams.page ?? "1", 10); + const usersData = await getAllUsers(page).catch(() => null); + + if (!usersData) { + return ( +
+

All Users

+

+ Could not load users. Please try again later. +

+
+ ); + } + + const { items, totalPages } = usersData; + + return ( +
+
+

All Users

+

+ Discover other users on Thoughts. +

+
+
+ + {totalPages > 1 && ( + + + + 1 ? `/users/all?page=${page - 1}` : "#"} + aria-disabled={page <= 1} + /> + + + = totalPages} + /> + + + + )} +
+
+ ); +} diff --git a/thoughts-frontend/components/main-nav.tsx b/thoughts-frontend/components/main-nav.tsx index e950325..aa78bb0 100644 --- a/thoughts-frontend/components/main-nav.tsx +++ b/thoughts-frontend/components/main-nav.tsx @@ -10,13 +10,13 @@ export function MainNav() { return ( diff --git a/thoughts-frontend/components/users-count.tsx b/thoughts-frontend/components/users-count.tsx new file mode 100644 index 0000000..fdb681d --- /dev/null +++ b/thoughts-frontend/components/users-count.tsx @@ -0,0 +1,69 @@ +import { Link } from "lucide-react"; +import { + Card, + CardHeader, + CardTitle, + CardContent, + CardDescription, +} from "@/components/ui/card"; +import { getAllUsersCount } from "@/lib/api"; + +export async function UsersCount() { + const usersCount = await getAllUsersCount().catch(() => null); + + if (usersCount === null) { + return ( + + + Users Count + + Total number of registered users on Thoughts. + + + +
+ Could not load users count. +
+
+
+ ); + } + + if (usersCount.count === 0) { + return ( + + + Users Count + + Total number of registered users on Thoughts. + + + +
+ No registered users yet. Be the first to{" "} + + sign up + + ! +
+
+
+ ); + } + + return ( + + + Users Count + + Total number of registered users on Thoughts. + + + +
+ {usersCount.count} registered users. +
+
+
+ ); +} diff --git a/thoughts-frontend/lib/api.ts b/thoughts-frontend/lib/api.ts index a777390..1cd9a42 100644 --- a/thoughts-frontend/lib/api.ts +++ b/thoughts-frontend/lib/api.ts @@ -327,4 +327,27 @@ export const deleteApiKey = (keyId: string, token: string) => ); export const getThoughtThread = (thoughtId: string, token: string | null) => - apiFetch(`/thoughts/${thoughtId}/thread`, {}, ThoughtThreadSchema, token); \ No newline at end of file + apiFetch(`/thoughts/${thoughtId}/thread`, {}, ThoughtThreadSchema, token); + + +export const getAllUsers = (page: number = 1, pageSize: number = 20) => + apiFetch( + `/users/all?page=${page}&page_size=${pageSize}`, + {}, + z.object({ + items: z.array(UserSchema), + page: z.number(), + pageSize: z.number(), + totalPages: z.number(), + totalItems: z.number(), + }) + ); + +export const getAllUsersCount = () => + apiFetch( + `/users/count`, + {}, + z.object({ + count: z.number(), + }) + ); \ No newline at end of file