Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components/pages/user/overview-tab/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { StatisticsCard } from "./statistics-card";
interface UserOverviewTabProps {
user: ApiTypes.User;
onSeeFavorites?: () => void;
onSeeProgress?: (contentType: ApiTypes.ReviewContentType) => void;
}

export function UserOverviewTab({ user, onSeeFavorites }: UserOverviewTabProps) {
export function UserOverviewTab({ user, onSeeFavorites, onSeeProgress }: UserOverviewTabProps) {
const session = useSession();

return (
<div className="flex flex-col gap-5">
<StatisticsCard user={user} />
<StatisticsCard user={user} onSeeProgress={onSeeProgress} />

<div className="flex max-sm:flex-col gap-5">
<div className="w-full md:w-2/3 flex flex-col gap-5">
Expand Down
24 changes: 20 additions & 4 deletions src/components/pages/user/overview-tab/statistics-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import type { ApiTypes } from "@/lib/api";

interface StatisticsCardProps {
user: ApiTypes.User;
onSeeProgress?: (contentType: ApiTypes.ReviewContentType) => void;
}

type MediaKey = keyof ApiTypes.User["progressStats"];

interface MediaConfig {
key: MediaKey;
contentType: ApiTypes.ReviewContentType;
icon: string;
label: string;
gradient: string;
Expand All @@ -21,6 +23,7 @@ interface MediaConfig {
const MEDIA_CONFIG: MediaConfig[] = [
{
key: "anime",
contentType: "anime",
icon: "lucide:mountain",
label: "common:types.anime_other",
gradient: "from-purple-500/20",
Expand All @@ -29,6 +32,7 @@ const MEDIA_CONFIG: MediaConfig[] = [
},
{
key: "manga",
contentType: "manga",
icon: "lucide:library-big",
label: "common:types.manga_other",
gradient: "from-rose-500/20",
Expand All @@ -37,6 +41,7 @@ const MEDIA_CONFIG: MediaConfig[] = [
},
{
key: "tvShow",
contentType: "tv",
icon: "lucide:tv-minimal-play",
label: "common:types.tv_other",
gradient: "from-blue-500/20",
Expand All @@ -45,6 +50,7 @@ const MEDIA_CONFIG: MediaConfig[] = [
},
{
key: "movie",
contentType: "movie",
icon: "lucide:clapperboard",
label: "common:types.movie_other",
gradient: "from-amber-500/20",
Expand All @@ -53,6 +59,7 @@ const MEDIA_CONFIG: MediaConfig[] = [
},
{
key: "game",
contentType: "game",
icon: "lucide:gamepad-2",
label: "common:types.game_other",
gradient: "from-emerald-500/20",
Expand All @@ -61,6 +68,7 @@ const MEDIA_CONFIG: MediaConfig[] = [
},
{
key: "book",
contentType: "book",
icon: "lucide:book",
label: "common:types.book_other",
gradient: "from-indigo-500/20",
Expand All @@ -76,18 +84,25 @@ function MediaTile({
border,
iconColor,
total,
onClick,
}: {
icon: string;
label: string;
gradient: string;
border: string;
iconColor: string;
total: number;
onClick?: () => void;
}) {
const { t } = useTranslation();

return (
<div className={`flex flex-col gap-3 rounded-2xl border bg-linear-to-br to-transparent p-4 ${gradient} ${border}`}>
<button
type="button"
onClick={onClick}
disabled={!onClick}
className={`flex flex-col gap-3 rounded-2xl border bg-linear-to-br to-transparent p-4 text-left transition-shadow enabled:cursor-pointer enabled:hover:shadow-lg ${gradient} ${border}`}
>
<div className="flex size-9 items-center justify-center rounded-xl bg-white/5">
<Icon icon={icon} className={`size-5 ${iconColor}`} />
</div>
Expand All @@ -96,11 +111,11 @@ function MediaTile({
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{t(label)}</span>
<span className="text-2xl font-bold text-card-foreground">{total}</span>
</div>
</div>
</button>
);
}

export function StatisticsCard({ user }: StatisticsCardProps) {
export function StatisticsCard({ user, onSeeProgress }: StatisticsCardProps) {
const { t } = useTranslation();

return (
Expand All @@ -115,7 +130,7 @@ export function StatisticsCard({ user }: StatisticsCardProps) {

<CardContent>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
{MEDIA_CONFIG.map(({ key, icon, label, gradient, border, iconColor }) => (
{MEDIA_CONFIG.map(({ key, contentType, icon, label, gradient, border, iconColor }) => (
<MediaTile
key={key}
icon={icon}
Expand All @@ -124,6 +139,7 @@ export function StatisticsCard({ user }: StatisticsCardProps) {
border={border}
iconColor={iconColor}
total={user.progressStats[key].total}
onClick={onSeeProgress ? () => onSeeProgress(contentType) : undefined}
/>
))}
</div>
Expand Down
124 changes: 124 additions & 0 deletions src/components/pages/user/progress-tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Icon } from "@iconify/react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { FavoriteCard, type FavoriteItem } from "@/components/pages/user/overview-tab/favorite-card";
import { Button } from "@/components/ui/button";
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { PROGRESS_CONTENT, progressStatusSections, progressToItem, useUserProgress } from "@/hooks/progress";
import type { ApiTypes } from "@/lib/api";

const CONTENT_TYPES: { type: ApiTypes.ReviewContentType; labelKey: string }[] = [
{ type: "game", labelKey: "common:types.game_other" },
{ type: "tv", labelKey: "common:types.tv_other" },
{ type: "anime", labelKey: "common:types.anime_other" },
{ type: "movie", labelKey: "common:types.movie_other" },
{ type: "manga", labelKey: "common:types.manga_other" },
{ type: "book", labelKey: "common:types.book_other" },
];

export function UserProgressTab({
userId,
progressStats,
contentType,
onContentTypeChange,
}: {
userId: string;
progressStats: ApiTypes.User["progressStats"];
contentType: ApiTypes.ReviewContentType;
onContentTypeChange: (contentType: ApiTypes.ReviewContentType) => void;
}) {
const { t } = useTranslation();

const progressQuery = useUserProgress(contentType, userId);

const rows = useMemo(() => progressQuery.data?.pages.flatMap((page) => page.items) ?? [], [progressQuery.data]);

const sections = useMemo(() => {
const typeStats = progressStats[PROGRESS_CONTENT[contentType].statsKey] as unknown as Record<
string,
{ count: number }
>;

return progressStatusSections(contentType)
.map((section) => ({
...section,
count: typeStats[section.statsKey]?.count ?? 0,
items: rows
.filter((row) => row.status === section.status)
.map((row) => progressToItem(contentType, row))
.filter((item): item is FavoriteItem => item !== null),
}))
.filter((section) => section.items.length > 0);
}, [rows, contentType, progressStats]);

return (
<div className="flex flex-col gap-5">
<div className="w-full sm:w-40">
<Select value={contentType} onValueChange={(value) => onContentTypeChange(value as ApiTypes.ReviewContentType)}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{CONTENT_TYPES.map(({ type, labelKey }) => (
<SelectItem key={type} value={type}>
{t(labelKey)}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>

{progressQuery.isLoading ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{Array.from({ length: 12 }).map((_, index) => (
<ProgressSkeleton key={index} />
))}
</div>
) : sections.length === 0 ? (
<Empty className="border-0">
<EmptyHeader>
<EmptyMedia variant="icon">
<Icon icon="lucide:chart-line" />
</EmptyMedia>
<EmptyTitle>{t("user:noProgress")}</EmptyTitle>
<EmptyDescription>{t("user:noProgressDescription")}</EmptyDescription>
</EmptyHeader>
</Empty>
) : (
sections.map((section) => (
<div key={section.status} className="flex flex-col gap-3">
<h4 className="text-lg font-semibold text-card-foreground">
{t(section.labelKey)} ({section.count})
</h4>

<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
{section.items.map((item) => (
<FavoriteCard key={item.id} item={item} />
))}
</div>
</div>
))
)}

{progressQuery.hasNextPage && (
<div className="flex justify-center">
<Button
variant="outline"
onClick={() => progressQuery.fetchNextPage()}
disabled={progressQuery.isFetchingNextPage}
>
{progressQuery.isFetchingNextPage ? t("user:loading") : t("user:loadMore")}
</Button>
</div>
)}
</div>
);
}

function ProgressSkeleton() {
return <Skeleton className="aspect-[3/4] w-full rounded-2xl" />;
}
Loading