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))
|
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> {
|
pub fn create_user_router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(users_get))
|
.route("/", get(users_get))
|
||||||
.route("/all", get(get_all_users_public))
|
.route("/all", get(get_all_users_public))
|
||||||
|
.route("/count", get(get_all_users_count))
|
||||||
.route("/me", get(get_me).put(update_me))
|
.route("/me", get(get_me).put(update_me))
|
||||||
.nest("/me/api-keys", create_api_key_router())
|
.nest("/me/api-keys", create_api_key_router())
|
||||||
.route("/{param}", get(get_user_by_param))
|
.route("/{param}", get(get_user_by_param))
|
||||||
|
@@ -180,3 +180,7 @@ pub async fn get_all_users(
|
|||||||
|
|
||||||
Ok((users, total_items))
|
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["page"], 2);
|
||||||
assert_eq!(v_p2["totalPages"], 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 { ThoughtThread } from "@/components/thought-thread";
|
||||||
import { buildThoughtThreads } from "@/lib/utils";
|
import { buildThoughtThreads } from "@/lib/utils";
|
||||||
import { TopFriends } from "@/components/top-friends";
|
import { TopFriends } from "@/components/top-friends";
|
||||||
|
import { UsersCount } from "@/components/users-count";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const token = (await cookies()).get("auth_token")?.value ?? null;
|
const token = (await cookies()).get("auth_token")?.value ?? null;
|
||||||
@@ -92,6 +93,7 @@ async function FeedPage({ token }: { token: string }) {
|
|||||||
)}
|
)}
|
||||||
<PopularTags />
|
<PopularTags />
|
||||||
{token && <TopFriends mode="friends" usernames={friends || []} />}
|
{token && <TopFriends mode="friends" usernames={friends || []} />}
|
||||||
|
<UsersCount />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<nav className="inline-flex md:flex items-center space-x-6 text-sm font-medium">
|
<nav className="inline-flex md:flex items-center space-x-6 text-sm font-medium">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/users/all"
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-colors hover:text-foreground/80",
|
"transition-colors hover:text-foreground/80",
|
||||||
pathname === "/" ? "text-foreground" : "text-foreground/60"
|
pathname === "/users/all" ? "text-foreground" : "text-foreground/60"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Feed
|
Discover
|
||||||
</Link>
|
</Link>
|
||||||
<SearchInput />
|
<SearchInput />
|
||||||
</nav>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
@@ -328,3 +328,26 @@ export const deleteApiKey = (keyId: string, token: string) =>
|
|||||||
|
|
||||||
export const getThoughtThread = (thoughtId: string, token: string | null) =>
|
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