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
All checks were successful
Build and Deploy Thoughts / build-and-deploy-local (push) Successful in 19s
This commit is contained in:
@@ -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))
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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>
|
||||
|
63
thoughts-frontend/app/users/all/page.tsx
Normal file
63
thoughts-frontend/app/users/all/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
|
69
thoughts-frontend/components/users-count.tsx
Normal file
69
thoughts-frontend/components/users-count.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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);
|
||||
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(),
|
||||
})
|
||||
);
|
Reference in New Issue
Block a user