diff --git a/src/api/generated/api.schemas.ts b/src/api/generated/api.schemas.ts index 68b7f4e..01ea4b0 100644 --- a/src/api/generated/api.schemas.ts +++ b/src/api/generated/api.schemas.ts @@ -89,6 +89,28 @@ export interface RefreshTokenRequestDTO { 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 { id?: number; /** @minLength 1 */ @@ -192,9 +214,9 @@ export interface PageMangaImportJobDTO { number?: number; pageable?: PageableObject; numberOfElements?: number; - sort?: SortObject; first?: boolean; last?: boolean; + sort?: SortObject; empty?: boolean; } @@ -254,9 +276,9 @@ export interface PageMangaListDTO { number?: number; pageable?: PageableObject; numberOfElements?: number; - sort?: SortObject; first?: boolean; last?: boolean; + sort?: SortObject; empty?: boolean; } diff --git a/src/api/generated/user-statistics/user-statistics.ts b/src/api/generated/user-statistics/user-statistics.ts new file mode 100644 index 0000000..eba1d00 --- /dev/null +++ b/src/api/generated/user-statistics/user-statistics.ts @@ -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 unknown> = Parameters[1]; + + + +/** + * Get statistics for the logged in user. + * @summary Get user statistics + */ +export const getUserStatistics = ( + + options?: SecondParameter,signal?: AbortSignal +) => { + + + return customInstance( + {url: `/user/statistics`, method: 'GET', signal + }, + options); + } + + + + +export const getGetUserStatisticsQueryKey = () => { + return [ + `/user/statistics` + ] as const; + } + + +export const getGetUserStatisticsQueryOptions = >, TError = unknown>( options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} +) => { + +const {query: queryOptions, request: requestOptions} = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetUserStatisticsQueryKey(); + + + + const queryFn: QueryFunction>> = ({ signal }) => getUserStatistics(requestOptions, signal); + + + + + + return { queryKey, queryFn, ...queryOptions} as UseQueryOptions>, TError, TData> & { queryKey: DataTag } +} + +export type GetUserStatisticsQueryResult = NonNullable>> +export type GetUserStatisticsQueryError = unknown + + +export function useGetUserStatistics>, TError = unknown>( + options: { query:Partial>, TError, TData>> & Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, request?: SecondParameter} + , queryClient?: QueryClient + ): DefinedUseQueryResult & { queryKey: DataTag } +export function useGetUserStatistics>, TError = unknown>( + options?: { query?:Partial>, TError, TData>> & Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + > , 'initialData' + >, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +export function useGetUserStatistics>, TError = unknown>( + options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } +/** + * @summary Get user statistics + */ + +export function useGetUserStatistics>, TError = unknown>( + options?: { query?:Partial>, TError, TData>>, request?: SecondParameter} + , queryClient?: QueryClient + ): UseQueryResult & { queryKey: DataTag } { + + const queryOptions = getGetUserStatisticsQueryOptions(options) + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { queryKey: DataTag }; + + query.queryKey = queryOptions.queryKey ; + + return query; +} + + + + diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index e4fafaa..5893eb4 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -1,210 +1,229 @@ -import { LogOut, Settings, User } from "lucide-react"; -import { useState } from "react"; -import { useNavigate } from "react-router"; -import { Button } from "@/components/ui/button"; +import {LogOut, Settings, User} from "lucide-react"; +import {useState} from "react"; +import {useNavigate} from "react-router"; +import {Button} from "@/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; +import {Input} from "@/components/ui/input"; import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, + Tabs, + TabsContent, + TabsList, + TabsTrigger, } 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 navigate = useNavigate(); - const { user, logout } = useAuth(); - const [itemsPerPage, setItemsPerPage] = useState(12); + const navigate = useNavigate(); + const {user, logout} = useAuth(); + const [itemsPerPage, setItemsPerPage] = useState(12); - if (!user) { - return ( -
- - - Access Denied - - Please log in to view your profile - - - - - - -
- ); - } + const {data} = useGetUserStatistics({query: {enabled: !!user}}); + const statistics = data?.data; - const handleLogout = () => { - logout(); - navigate("/"); - }; + if (!user) { + return ( +
+ + + Access Denied + + Please log in to view your profile + + + + + + +
+ ); + } - const handleSavePreferences = () => { - // updatePreferences({ itemsPerPage }); - }; + const handleLogout = () => { + logout(); + navigate("/"); + }; - return ( -
-
-
-

Profile

- -
+ const handleSavePreferences = () => { + // updatePreferences({ itemsPerPage }); + }; - - - - - Account - - - - Preferences - - Stats - + return ( +
+
+
+

Profile

+ +
- - - - Account Information - Your account details - - -
- - -
-
- - -
-
-
-
+ + + + + Account + + + + Preferences + + Stats + - - - - Preferences - Customize your experience - - -
- - - setItemsPerPage(Number.parseInt(e.target.value, 10)) - } - /> -

- Number of manga to display per page (6-48) -

-
- -
-
-
+ + + + Account Information + Your account details + + +
+ + +
+
+ + +
+
+
+
- - - - Reading Statistics - Your manga reading activity - - -
-
-

- Favorite Manga -

-

- {/*{user.favorites.length}*/} -

-
-
-

- Manga Reading -

-

- {/*{Object.keys(user.chaptersRead).length}*/} -

-
-
+ + + + Preferences + Customize your experience + + +
+ + + setItemsPerPage(Number.parseInt(e.target.value, 10)) + } + /> +

+ Number of manga to display per page (6-48) +

+
+ +
+
+
- {/*{user.favorites.length > 0 && (*/} - {/*
*/} - {/*

Favorite Manga IDs

*/} - {/*
*/} - {/* {user.favorites.map((id) => (*/} - {/* */} - {/* #{id}*/} - {/* */} - {/* ))}*/} - {/*
*/} - {/*
*/} - {/*)}*/} + +
+ + + Favorite Manga + + +
{statistics?.favoriteMangaCount}
+

Manga in your favorites

+
+
- {/*{Object.keys(user.chaptersRead).length > 0 && (*/} - {/*
*/} - {/*

Reading Progress

*/} - {/*
*/} - {/* {Object.entries(user.chaptersRead).map(*/} - {/* ([mangaId, chapter]) => (*/} - {/*

*/} - {/* Manga #{mangaId}: Chapter {chapter}*/} - {/*

*/} - {/* ),*/} - {/* )}*/} - {/*
*/} - {/*
*/} - {/*)}*/} + + + Manga Reading + + +
{statistics?.mangaReadingCount}
+

Manga you've started reading

+
+
- {/*{user.favorites.length === 0 &&*/} - {/* Object.keys(user.chaptersRead).length === 0 && (*/} - {/* */} - {/* */} - {/* No reading activity yet. Start adding favorites and*/} - {/* tracking chapters!*/} - {/* */} - {/* */} - {/* )}*/} - - - - -
-
- ); + + + Chapters Read + + +
{statistics?.chaptersReadCount}
+

Total chapters completed

+
+
+ + + + Last Read + + +
+ {statistics?.lastReadContent?.readAt + ? new Date(statistics.lastReadContent.readAt).toLocaleDateString() + : "Never"} +
+ {statistics?.lastReadContent && ( +
+ {statistics.lastReadContent.mangaTitle}: {statistics.lastReadContent.contentTitle} +
) + } +

Your most recent reading + activity

+
+
+
+ + {statistics?.recentActivities && statistics?.recentActivities.length > 0 && ( + + + Reading Progress + Your progress across manga titles + + + {statistics.recentActivities.map((activity) => ( +
+ + {activity.mangaTitle} + + + {activity.contentTitle} + +
+ ))} +
+
+ )} + + {!statistics?.hasReadingActivity && ( + + + No reading activity yet. Start exploring manga and tracking your progress! + + + )} + +
+
+
+ ); }; export default Profile;