feat: add user count endpoint and integrate it into frontend components
All checks were successful
Build and Deploy Thoughts / build-and-deploy-local (push) Successful in 19s

This commit is contained in:
2025-09-09 03:07:48 +02:00
parent d92c9a747e
commit 4ea4f3149f
8 changed files with 193 additions and 4 deletions

View File

@@ -451,10 +451,16 @@ async fn get_all_users_public(
Ok(Json(response))
}
async fn get_all_users_count(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
let count = app::persistence::user::get_all_users_count(&state.conn).await?;
Ok(Json(json!({ "count": count })))
}
pub fn create_user_router() -> Router<AppState> {
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))

View File

@@ -180,3 +180,7 @@ pub async fn get_all_users(
Ok((users, total_items))
}
pub async fn get_all_users_count(db: &DbConn) -> Result<u64, DbErr> {
user::Entity::find().count(db).await
}

View File

@@ -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);
}

View File

@@ -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 }) {
)}
<PopularTags />
{token && <TopFriends mode="friends" usernames={friends || []} />}
<UsersCount />
</div>
</aside>
</div>

View File

@@ -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 (
<div className="container mx-auto max-w-2xl p-4 sm:p-6 text-center">
<h1 className="text-3xl font-bold my-6">All Users</h1>
<p className="text-muted-foreground">
Could not load users. Please try again later.
</p>
</div>
);
}
const { items, totalPages } = usersData;
return (
<div className="container mx-auto max-w-2xl p-4 sm:p-6">
<header className="my-6 glass-effect glossy-effect bottom gloss-highlight rounded-md p-4 text-shadow-md">
<h1 className="text-3xl font-bold">All Users</h1>
<p className="text-muted-foreground">
Discover other users on Thoughts.
</p>
</header>
<main>
<UserListCard users={items} />
{totalPages > 1 && (
<Pagination className="mt-8">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href={page > 1 ? `/users/all?page=${page - 1}` : "#"}
aria-disabled={page <= 1}
/>
</PaginationItem>
<PaginationItem>
<PaginationNext
href={page < totalPages ? `/users/all?page=${page + 1}` : "#"}
aria-disabled={page >= totalPages}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</main>
</div>
);
}

View File

@@ -10,13 +10,13 @@ export function MainNav() {
return (
<nav className="inline-flex md:flex items-center space-x-6 text-sm font-medium">
<Link
href="/"
href="/users/all"
className={cn(
"transition-colors hover:text-foreground/80",
pathname === "/" ? "text-foreground" : "text-foreground/60"
pathname === "/users/all" ? "text-foreground" : "text-foreground/60"
)}
>
Feed
Discover
</Link>
<SearchInput />
</nav>

View File

@@ -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 (
<Card className="p-4">
<CardHeader className="p-0 pb-2">
<CardTitle className="text-lg text-shadow-md">Users Count</CardTitle>
<CardDescription>
Total number of registered users on Thoughts.
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-muted-foreground text-sm text-center py-4">
Could not load users count.
</div>
</CardContent>
</Card>
);
}
if (usersCount.count === 0) {
return (
<Card className="p-4">
<CardHeader className="p-0 pb-2">
<CardTitle className="text-lg text-shadow-md">Users Count</CardTitle>
<CardDescription>
Total number of registered users on Thoughts.
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-muted-foreground text-sm text-center py-4">
No registered users yet. Be the first to{" "}
<Link href="/signup" className="text-primary hover:underline">
sign up
</Link>
!
</div>
</CardContent>
</Card>
);
}
return (
<Card className="p-4">
<CardHeader className="p-0 pb-2">
<CardTitle className="text-lg text-shadow-md">Users Count</CardTitle>
<CardDescription>
Total number of registered users on Thoughts.
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-muted-foreground text-sm text-center py-4">
{usersCount.count} registered users.
</div>
</CardContent>
</Card>
);
}

View File

@@ -328,3 +328,26 @@ export const deleteApiKey = (keyId: string, token: string) =>
export const getThoughtThread = (thoughtId: string, token: string | null) =>
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(),
})
);