Merge pull request 'refactor: Enhance Profile component with user statistics and improved reading progress display' (#30) from refactor into main
Reviewed-on: #30
This commit is contained in:
commit
ee47a05f9f
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
125
src/api/generated/user-statistics/user-statistics.ts
Normal file
125
src/api/generated/user-statistics/user-statistics.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,210 +1,229 @@
|
|||||||
import { LogOut, Settings, User } from "lucide-react";
|
import {LogOut, Settings, User} from "lucide-react";
|
||||||
import { useState } from "react";
|
import {useState} from "react";
|
||||||
import { useNavigate } from "react-router";
|
import {useNavigate} from "react-router";
|
||||||
import { Button } from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
TabsList,
|
TabsList,
|
||||||
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);
|
||||||
|
|
||||||
if (!user) {
|
const {data} = useGetUserStatistics({query: {enabled: !!user}});
|
||||||
return (
|
const statistics = data?.data;
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
|
||||||
<Card className="w-full max-w-md">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Access Denied</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Please log in to view your profile
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Button onClick={() => navigate("/login")} className="w-full">
|
|
||||||
Go to Login
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
if (!user) {
|
||||||
logout();
|
return (
|
||||||
navigate("/");
|
<div className="flex min-h-screen items-center justify-center bg-background">
|
||||||
};
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Access Denied</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Please log in to view your profile
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Button onClick={() => navigate("/login")} className="w-full">
|
||||||
|
Go to Login
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const handleSavePreferences = () => {
|
const handleLogout = () => {
|
||||||
// updatePreferences({ itemsPerPage });
|
logout();
|
||||||
};
|
navigate("/");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const handleSavePreferences = () => {
|
||||||
<div className="min-h-screen bg-background">
|
// updatePreferences({ itemsPerPage });
|
||||||
<div className="mx-auto max-w-2xl px-4 py-8">
|
};
|
||||||
<div className="mb-8 flex items-center justify-between">
|
|
||||||
<h1 className="text-3xl font-bold">Profile</h1>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="gap-2 bg-transparent"
|
|
||||||
>
|
|
||||||
<LogOut className="h-4 w-4" />
|
|
||||||
Logout
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Tabs defaultValue="account" className="space-y-6">
|
return (
|
||||||
<TabsList className="grid w-full grid-cols-3">
|
<div className="min-h-screen bg-background">
|
||||||
<TabsTrigger value="account" className="gap-2">
|
<div className="mx-auto max-w-2xl px-4 py-8">
|
||||||
<User className="h-4 w-4" />
|
<div className="mb-8 flex items-center justify-between">
|
||||||
Account
|
<h1 className="text-3xl font-bold">Profile</h1>
|
||||||
</TabsTrigger>
|
<Button
|
||||||
<TabsTrigger value="preferences" className="gap-2">
|
variant="outline"
|
||||||
<Settings className="h-4 w-4" />
|
onClick={handleLogout}
|
||||||
Preferences
|
className="gap-2 bg-transparent"
|
||||||
</TabsTrigger>
|
>
|
||||||
<TabsTrigger value="stats">Stats</TabsTrigger>
|
<LogOut className="h-4 w-4"/>
|
||||||
</TabsList>
|
Logout
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TabsContent value="account" className="space-y-4">
|
<Tabs defaultValue="account" className="space-y-6">
|
||||||
<Card>
|
<TabsList className="grid w-full grid-cols-3">
|
||||||
<CardHeader>
|
<TabsTrigger value="account" className="gap-2">
|
||||||
<CardTitle>Account Information</CardTitle>
|
<User className="h-4 w-4"/>
|
||||||
<CardDescription>Your account details</CardDescription>
|
Account
|
||||||
</CardHeader>
|
</TabsTrigger>
|
||||||
<CardContent className="space-y-4">
|
<TabsTrigger value="preferences" className="gap-2">
|
||||||
<div className="space-y-2">
|
<Settings className="h-4 w-4"/>
|
||||||
<label className="text-sm font-medium">Username</label>
|
Preferences
|
||||||
<Input value={user.name} disabled />
|
</TabsTrigger>
|
||||||
</div>
|
<TabsTrigger value="stats">Stats</TabsTrigger>
|
||||||
<div className="space-y-2">
|
</TabsList>
|
||||||
<label className="text-sm font-medium">Email</label>
|
|
||||||
<Input value={user.email} disabled />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="preferences" className="space-y-4">
|
<TabsContent value="account" className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Preferences</CardTitle>
|
<CardTitle>Account Information</CardTitle>
|
||||||
<CardDescription>Customize your experience</CardDescription>
|
<CardDescription>Your account details</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="itemsPerPage" className="text-sm font-medium">
|
<label className="text-sm font-medium">Username</label>
|
||||||
Items Per Page
|
<Input value={user.name} disabled/>
|
||||||
</label>
|
</div>
|
||||||
<Input
|
<div className="space-y-2">
|
||||||
id="itemsPerPage"
|
<label className="text-sm font-medium">Email</label>
|
||||||
type="number"
|
<Input value={user.email} disabled/>
|
||||||
min="6"
|
</div>
|
||||||
max="48"
|
</CardContent>
|
||||||
step="6"
|
</Card>
|
||||||
value={itemsPerPage}
|
</TabsContent>
|
||||||
onChange={(e) =>
|
|
||||||
setItemsPerPage(Number.parseInt(e.target.value, 10))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Number of manga to display per page (6-48)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button onClick={handleSavePreferences}>
|
|
||||||
Save Preferences
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="stats" className="space-y-4">
|
<TabsContent value="preferences" className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Reading Statistics</CardTitle>
|
<CardTitle>Preferences</CardTitle>
|
||||||
<CardDescription>Your manga reading activity</CardDescription>
|
<CardDescription>Customize your experience</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="space-y-2">
|
||||||
<div className="rounded-lg bg-card p-4">
|
<label htmlFor="itemsPerPage" className="text-sm font-medium">
|
||||||
<p className="text-sm text-muted-foreground">
|
Items Per Page
|
||||||
Favorite Manga
|
</label>
|
||||||
</p>
|
<Input
|
||||||
<p className="text-2xl font-bold">
|
id="itemsPerPage"
|
||||||
{/*{user.favorites.length}*/}
|
type="number"
|
||||||
</p>
|
min="6"
|
||||||
</div>
|
max="48"
|
||||||
<div className="rounded-lg bg-card p-4">
|
step="6"
|
||||||
<p className="text-sm text-muted-foreground">
|
value={itemsPerPage}
|
||||||
Manga Reading
|
onChange={(e) =>
|
||||||
</p>
|
setItemsPerPage(Number.parseInt(e.target.value, 10))
|
||||||
<p className="text-2xl font-bold">
|
}
|
||||||
{/*{Object.keys(user.chaptersRead).length}*/}
|
/>
|
||||||
</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
</div>
|
Number of manga to display per page (6-48)
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleSavePreferences}>
|
||||||
|
Save Preferences
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
{/*{user.favorites.length > 0 && (*/}
|
<TabsContent value="stats" className="space-y-4">
|
||||||
{/* <div className="space-y-2">*/}
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{/* <h3 className="font-semibold">Favorite Manga IDs</h3>*/}
|
<Card>
|
||||||
{/* <div className="flex flex-wrap gap-2">*/}
|
<CardHeader className="pb-3">
|
||||||
{/* {user.favorites.map((id) => (*/}
|
<CardTitle className="text-sm font-medium">Favorite Manga</CardTitle>
|
||||||
{/* <span*/}
|
</CardHeader>
|
||||||
{/* key={id}*/}
|
<CardContent>
|
||||||
{/* className="rounded-full bg-primary/10 px-3 py-1 text-sm"*/}
|
<div className="text-3xl font-bold">{statistics?.favoriteMangaCount}</div>
|
||||||
{/* >*/}
|
<p className="text-xs text-muted-foreground mt-1">Manga in your favorites</p>
|
||||||
{/* #{id}*/}
|
</CardContent>
|
||||||
{/* </span>*/}
|
</Card>
|
||||||
{/* ))}*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
{/*)}*/}
|
|
||||||
|
|
||||||
{/*{Object.keys(user.chaptersRead).length > 0 && (*/}
|
<Card>
|
||||||
{/* <div className="space-y-2">*/}
|
<CardHeader className="pb-3">
|
||||||
{/* <h3 className="font-semibold">Reading Progress</h3>*/}
|
<CardTitle className="text-sm font-medium">Manga Reading</CardTitle>
|
||||||
{/* <div className="space-y-1 text-sm">*/}
|
</CardHeader>
|
||||||
{/* {Object.entries(user.chaptersRead).map(*/}
|
<CardContent>
|
||||||
{/* ([mangaId, chapter]) => (*/}
|
<div className="text-3xl font-bold">{statistics?.mangaReadingCount}</div>
|
||||||
{/* <p key={mangaId} className="text-muted-foreground">*/}
|
<p className="text-xs text-muted-foreground mt-1">Manga you've started reading</p>
|
||||||
{/* Manga #{mangaId}: Chapter {chapter}*/}
|
</CardContent>
|
||||||
{/* </p>*/}
|
</Card>
|
||||||
{/* ),*/}
|
|
||||||
{/* )}*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
{/* </div>*/}
|
|
||||||
{/*)}*/}
|
|
||||||
|
|
||||||
{/*{user.favorites.length === 0 &&*/}
|
<Card>
|
||||||
{/* Object.keys(user.chaptersRead).length === 0 && (*/}
|
<CardHeader className="pb-3">
|
||||||
{/* <Alert>*/}
|
<CardTitle className="text-sm font-medium">Chapters Read</CardTitle>
|
||||||
{/* <AlertDescription>*/}
|
</CardHeader>
|
||||||
{/* No reading activity yet. Start adding favorites and*/}
|
<CardContent>
|
||||||
{/* tracking chapters!*/}
|
<div className="text-3xl font-bold">{statistics?.chaptersReadCount}</div>
|
||||||
{/* </AlertDescription>*/}
|
<p className="text-xs text-muted-foreground mt-1">Total chapters completed</p>
|
||||||
{/* </Alert>*/}
|
</CardContent>
|
||||||
{/* )}*/}
|
</Card>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
<Card>
|
||||||
</TabsContent>
|
<CardHeader className="pb-3">
|
||||||
</Tabs>
|
<CardTitle className="text-sm font-medium">Last Read</CardTitle>
|
||||||
</div>
|
</CardHeader>
|
||||||
</div>
|
<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>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Profile;
|
export default Profile;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user