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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
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 { 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 (
|
||||
<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 {data} = useGetUserStatistics({query: {enabled: !!user}});
|
||||
const statistics = data?.data;
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate("/");
|
||||
};
|
||||
if (!user) {
|
||||
return (
|
||||
<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 = () => {
|
||||
// updatePreferences({ itemsPerPage });
|
||||
};
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<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>
|
||||
const handleSavePreferences = () => {
|
||||
// updatePreferences({ itemsPerPage });
|
||||
};
|
||||
|
||||
<Tabs defaultValue="account" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="account" className="gap-2">
|
||||
<User className="h-4 w-4" />
|
||||
Account
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="preferences" className="gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
Preferences
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="stats">Stats</TabsTrigger>
|
||||
</TabsList>
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<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>
|
||||
|
||||
<TabsContent value="account" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Information</CardTitle>
|
||||
<CardDescription>Your account details</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Username</label>
|
||||
<Input value={user.name} disabled />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Email</label>
|
||||
<Input value={user.email} disabled />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<Tabs defaultValue="account" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="account" className="gap-2">
|
||||
<User className="h-4 w-4"/>
|
||||
Account
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="preferences" className="gap-2">
|
||||
<Settings className="h-4 w-4"/>
|
||||
Preferences
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="stats">Stats</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="preferences" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Preferences</CardTitle>
|
||||
<CardDescription>Customize your experience</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="itemsPerPage" className="text-sm font-medium">
|
||||
Items Per Page
|
||||
</label>
|
||||
<Input
|
||||
id="itemsPerPage"
|
||||
type="number"
|
||||
min="6"
|
||||
max="48"
|
||||
step="6"
|
||||
value={itemsPerPage}
|
||||
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="account" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Account Information</CardTitle>
|
||||
<CardDescription>Your account details</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Username</label>
|
||||
<Input value={user.name} disabled/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Email</label>
|
||||
<Input value={user.email} disabled/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="stats" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Reading Statistics</CardTitle>
|
||||
<CardDescription>Your manga reading activity</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="rounded-lg bg-card p-4">
|
||||
<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>
|
||||
<TabsContent value="preferences" className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Preferences</CardTitle>
|
||||
<CardDescription>Customize your experience</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="itemsPerPage" className="text-sm font-medium">
|
||||
Items Per Page
|
||||
</label>
|
||||
<Input
|
||||
id="itemsPerPage"
|
||||
type="number"
|
||||
min="6"
|
||||
max="48"
|
||||
step="6"
|
||||
value={itemsPerPage}
|
||||
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>
|
||||
|
||||
{/*{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>*/}
|
||||
{/*)}*/}
|
||||
<TabsContent value="stats" className="space-y-4">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm font-medium">Favorite Manga</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">{statistics?.favoriteMangaCount}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">Manga in your favorites</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/*{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>*/}
|
||||
{/*)}*/}
|
||||
<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>
|
||||
|
||||
{/*{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>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<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>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user