Compare commits

...

2 Commits

3 changed files with 358 additions and 192 deletions

View File

@ -89,6 +89,28 @@ export interface RefreshTokenRequestDTO {
refreshToken: string; refreshToken: string;
} }
export interface DefaultResponseDTOUserStatisticsDTO {
timestamp?: string;
data?: UserStatisticsDTO;
message?: string;
}
export interface UserRecentActivityDTO {
mangaTitle?: string;
contentTitle?: string;
readAt?: string;
mangaId?: number;
}
export interface UserStatisticsDTO {
favoriteMangaCount?: number;
mangaReadingCount?: number;
chaptersReadCount?: number;
lastReadContent?: UserRecentActivityDTO;
hasReadingActivity?: boolean;
recentActivities?: UserRecentActivityDTO[];
}
export interface ContentProviderDTO { export interface ContentProviderDTO {
id?: number; id?: number;
/** @minLength 1 */ /** @minLength 1 */
@ -192,9 +214,9 @@ export interface PageMangaImportJobDTO {
number?: number; number?: number;
pageable?: PageableObject; pageable?: PageableObject;
numberOfElements?: number; numberOfElements?: number;
sort?: SortObject;
first?: boolean; first?: boolean;
last?: boolean; last?: boolean;
sort?: SortObject;
empty?: boolean; empty?: boolean;
} }
@ -254,9 +276,9 @@ export interface PageMangaListDTO {
number?: number; number?: number;
pageable?: PageableObject; pageable?: PageableObject;
numberOfElements?: number; numberOfElements?: number;
sort?: SortObject;
first?: boolean; first?: boolean;
last?: boolean; last?: boolean;
sort?: SortObject;
empty?: boolean; empty?: boolean;
} }

View File

@ -0,0 +1,125 @@
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* OpenAPI definition
* OpenAPI spec version: v0
*/
import {
useQuery
} from '@tanstack/react-query';
import type {
DataTag,
DefinedInitialDataOptions,
DefinedUseQueryResult,
QueryClient,
QueryFunction,
QueryKey,
UndefinedInitialDataOptions,
UseQueryOptions,
UseQueryResult
} from '@tanstack/react-query';
import type {
DefaultResponseDTOUserStatisticsDTO
} from '../api.schemas';
import { customInstance } from '../../api';
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* Get statistics for the logged in user.
* @summary Get user statistics
*/
export const getUserStatistics = (
options?: SecondParameter<typeof customInstance>,signal?: AbortSignal
) => {
return customInstance<DefaultResponseDTOUserStatisticsDTO>(
{url: `/user/statistics`, method: 'GET', signal
},
options);
}
export const getGetUserStatisticsQueryKey = () => {
return [
`/user/statistics`
] as const;
}
export const getGetUserStatisticsQueryOptions = <TData = Awaited<ReturnType<typeof getUserStatistics>>, TError = unknown>( options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUserStatistics>>, TError, TData>>, request?: SecondParameter<typeof customInstance>}
) => {
const {query: queryOptions, request: requestOptions} = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetUserStatisticsQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof getUserStatistics>>> = ({ signal }) => getUserStatistics(requestOptions, signal);
return { queryKey, queryFn, ...queryOptions} as UseQueryOptions<Awaited<ReturnType<typeof getUserStatistics>>, TError, TData> & { queryKey: DataTag<QueryKey, TData, TError> }
}
export type GetUserStatisticsQueryResult = NonNullable<Awaited<ReturnType<typeof getUserStatistics>>>
export type GetUserStatisticsQueryError = unknown
export function useGetUserStatistics<TData = Awaited<ReturnType<typeof getUserStatistics>>, TError = unknown>(
options: { query:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUserStatistics>>, TError, TData>> & Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof getUserStatistics>>,
TError,
Awaited<ReturnType<typeof getUserStatistics>>
> , 'initialData'
>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetUserStatistics<TData = Awaited<ReturnType<typeof getUserStatistics>>, TError = unknown>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUserStatistics>>, TError, TData>> & Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof getUserStatistics>>,
TError,
Awaited<ReturnType<typeof getUserStatistics>>
> , 'initialData'
>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
export function useGetUserStatistics<TData = Awaited<ReturnType<typeof getUserStatistics>>, TError = unknown>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUserStatistics>>, TError, TData>>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> }
/**
* @summary Get user statistics
*/
export function useGetUserStatistics<TData = Awaited<ReturnType<typeof getUserStatistics>>, TError = unknown>(
options?: { query?:Partial<UseQueryOptions<Awaited<ReturnType<typeof getUserStatistics>>, TError, TData>>, request?: SecondParameter<typeof customInstance>}
, queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getGetUserStatisticsQueryOptions(options)
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
query.queryKey = queryOptions.queryKey ;
return query;
}

View File

@ -17,12 +17,17 @@ import {
TabsTrigger, TabsTrigger,
} from "@/components/ui/tabs.tsx"; } from "@/components/ui/tabs.tsx";
import {useAuth} from "@/contexts/AuthContext.tsx"; import {useAuth} from "@/contexts/AuthContext.tsx";
import {useGetUserStatistics} from "@/api/generated/user-statistics/user-statistics.ts";
import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
const Profile = () => { const Profile = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const {user, logout} = useAuth(); const {user, logout} = useAuth();
const [itemsPerPage, setItemsPerPage] = useState(12); const [itemsPerPage, setItemsPerPage] = useState(12);
const {data} = useGetUserStatistics({query: {enabled: !!user}});
const statistics = data?.data;
if (!user) { if (!user) {
return ( return (
<div className="flex min-h-screen items-center justify-center bg-background"> <div className="flex min-h-screen items-center justify-center bg-background">
@ -133,73 +138,87 @@ const Profile = () => {
</TabsContent> </TabsContent>
<TabsContent value="stats" className="space-y-4"> <TabsContent value="stats" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<Card> <Card>
<CardHeader> <CardHeader className="pb-3">
<CardTitle>Reading Statistics</CardTitle> <CardTitle className="text-sm font-medium">Favorite Manga</CardTitle>
<CardDescription>Your manga reading activity</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent>
<div className="grid grid-cols-2 gap-4"> <div className="text-3xl font-bold">{statistics?.favoriteMangaCount}</div>
<div className="rounded-lg bg-card p-4"> <p className="text-xs text-muted-foreground mt-1">Manga in your favorites</p>
<p className="text-sm text-muted-foreground">
Favorite Manga
</p>
<p className="text-2xl font-bold">
{/*{user.favorites.length}*/}
</p>
</div>
<div className="rounded-lg bg-card p-4">
<p className="text-sm text-muted-foreground">
Manga Reading
</p>
<p className="text-2xl font-bold">
{/*{Object.keys(user.chaptersRead).length}*/}
</p>
</div>
</div>
{/*{user.favorites.length > 0 && (*/}
{/* <div className="space-y-2">*/}
{/* <h3 className="font-semibold">Favorite Manga IDs</h3>*/}
{/* <div className="flex flex-wrap gap-2">*/}
{/* {user.favorites.map((id) => (*/}
{/* <span*/}
{/* key={id}*/}
{/* className="rounded-full bg-primary/10 px-3 py-1 text-sm"*/}
{/* >*/}
{/* #{id}*/}
{/* </span>*/}
{/* ))}*/}
{/* </div>*/}
{/* </div>*/}
{/*)}*/}
{/*{Object.keys(user.chaptersRead).length > 0 && (*/}
{/* <div className="space-y-2">*/}
{/* <h3 className="font-semibold">Reading Progress</h3>*/}
{/* <div className="space-y-1 text-sm">*/}
{/* {Object.entries(user.chaptersRead).map(*/}
{/* ([mangaId, chapter]) => (*/}
{/* <p key={mangaId} className="text-muted-foreground">*/}
{/* Manga #{mangaId}: Chapter {chapter}*/}
{/* </p>*/}
{/* ),*/}
{/* )}*/}
{/* </div>*/}
{/* </div>*/}
{/*)}*/}
{/*{user.favorites.length === 0 &&*/}
{/* Object.keys(user.chaptersRead).length === 0 && (*/}
{/* <Alert>*/}
{/* <AlertDescription>*/}
{/* No reading activity yet. Start adding favorites and*/}
{/* tracking chapters!*/}
{/* </AlertDescription>*/}
{/* </Alert>*/}
{/* )}*/}
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Manga Reading</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">{statistics?.mangaReadingCount}</div>
<p className="text-xs text-muted-foreground mt-1">Manga you've started reading</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Chapters Read</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold">{statistics?.chaptersReadCount}</div>
<p className="text-xs text-muted-foreground mt-1">Total chapters completed</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium">Last Read</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm font-medium">
{statistics?.lastReadContent?.readAt
? new Date(statistics.lastReadContent.readAt).toLocaleDateString()
: "Never"}
</div>
{statistics?.lastReadContent && (
<div className="text-sm font-light">
{statistics.lastReadContent.mangaTitle}: {statistics.lastReadContent.contentTitle}
</div>)
}
<p className="text-xs text-muted-foreground mt-1">Your most recent reading
activity</p>
</CardContent>
</Card>
</div>
{statistics?.recentActivities && statistics?.recentActivities.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Reading Progress</CardTitle>
<CardDescription>Your progress across manga titles</CardDescription>
</CardHeader>
<CardContent>
{statistics.recentActivities.map((activity) => (
<div key={`${activity.mangaId}-${activity.readAt}`}
className="flex items-center justify-between rounded-lg bg-card p-3">
<span className="text-sm font-medium">
{activity.mangaTitle}
</span>
<span className="text-sm text-muted-foreground">
{activity.contentTitle}
</span>
</div>
))}
</CardContent>
</Card>
)}
{!statistics?.hasReadingActivity && (
<Alert>
<AlertDescription>
No reading activity yet. Start exploring manga and tracking your progress!
</AlertDescription>
</Alert>
)}
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>