diff --git a/src/app/Http/Controllers/Anime/AnimeController.php b/src/app/Http/Controllers/Anime/AnimeController.php index 477d358..eb6b6c3 100644 --- a/src/app/Http/Controllers/Anime/AnimeController.php +++ b/src/app/Http/Controllers/Anime/AnimeController.php @@ -93,10 +93,14 @@ public function show(Request $request, Anime $anime): Response 'genres:name', ]); - $animeListEntry = $this->userAnimeListService->findById($request->user(), $anime->id); - $animeListStatuses = AnimeListStatusEnum::cases(); + $animeListEntry = $this->userAnimeListService->findById($request->user(), $anime->id); + $animeListStatuses = AnimeListStatusEnum::cases(); + $animeListStatistic = $this->userAnimeListService->animeStatistics($anime->id); - return Inertia::render('Anime/Show', compact('anime', 'animeListEntry', 'animeListStatuses')); + return Inertia::render( + 'Anime/Show', + compact('anime', 'animeListEntry', 'animeListStatuses', 'animeListStatistic') + ); } /** diff --git a/src/app/Services/User/UserAnimeListService.php b/src/app/Services/User/UserAnimeListService.php index eb2f266..7ac2c0d 100644 --- a/src/app/Services/User/UserAnimeListService.php +++ b/src/app/Services/User/UserAnimeListService.php @@ -8,6 +8,7 @@ use App\Enums\UserAnimeList\StatusEnum; use App\Models\Pivots\UserAnimeList; use App\Models\User; +use Illuminate\Support\Facades\DB; final class UserAnimeListService { @@ -17,6 +18,53 @@ public function findById(User $user, string $animeId): ?UserAnimeList return $user->animeList()->where('anime_id', $animeId)->first()?->pivot; } + /** + * @return array{status: string, user_count: int, percentage: float} + */ + public function animeStatistics(string $animeId): array + { + $subQuery = UserAnimeList::query()->fromRaw( + "( + SELECT 'plan_to_watch' AS status + UNION ALL SELECT 'watching' + UNION ALL SELECT 'on_hold' + UNION ALL SELECT 'completed' + UNION ALL SELECT 'dropped' + ) as statuses" + ); + + // Not using model directly because of Model::shouldBeStrict() + return DB::table(UserAnimeList::class) + ->fromSub( + $subQuery->leftJoin( + 'user_anime_list as ual', + static fn($join) => $join->on( + 'statuses.status', + '=', + 'ual.status' + )->where('ual.anime_id', $animeId) + ) + ->groupBy('statuses.status') + ->select([ + 'statuses.status', + DB::raw('COUNT(ual.user_id) as user_count'), + DB::raw('SUM(COUNT(ual.user_id)) OVER() as total_count'), + ]), + 'sub' + ) + ->select([ + 'status', + 'user_count', + DB::raw('ROUND(user_count * 100 / total_count, 1) as percentage'), + ]) + ->get() + ->map(static function (\stdClass $item) { + $item->percentage = (float) $item->percentage; + return (array) $item; + }) + ->toArray(); + } + public function addAnime(User $user, string $animeId): void { $user->animeList()->attach($animeId, ['status' => StatusEnum::PLAN_TO_WATCH]); diff --git a/src/resources/js/entities/anime-list/model/index.ts b/src/resources/js/entities/anime-list/model/index.ts index 8022df1..2aa6b88 100644 --- a/src/resources/js/entities/anime-list/model/index.ts +++ b/src/resources/js/entities/anime-list/model/index.ts @@ -1 +1,7 @@ -export { Status, type AnimeList, type AnimeListEntry } from './types'; +export { + Status, + type AnimeList, + type AnimeListEntry, + type AnimeListEntryStatistic, + type AnimeListStatistics, +} from './types'; diff --git a/src/resources/js/entities/anime-list/model/types.ts b/src/resources/js/entities/anime-list/model/types.ts index baea627..628729e 100644 --- a/src/resources/js/entities/anime-list/model/types.ts +++ b/src/resources/js/entities/anime-list/model/types.ts @@ -14,4 +14,18 @@ type AnimeListEntry = { type AnimeList = AnimeListEntry[]; -export { Status, type AnimeListEntry, type AnimeList }; +type AnimeListEntryStatistic = { + status: Status; + user_count: number; + percentage: number; +}; + +type AnimeListStatistics = AnimeListEntryStatistic[]; + +export { + Status, + type AnimeListEntry, + type AnimeList, + type AnimeListEntryStatistic, + type AnimeListStatistics, +}; diff --git a/src/resources/js/features/anime/list-statistic/AnimeListStatistic.vue b/src/resources/js/features/anime/list-statistic/AnimeListStatistic.vue new file mode 100644 index 0000000..3fff24b --- /dev/null +++ b/src/resources/js/features/anime/list-statistic/AnimeListStatistic.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/resources/js/features/anime/list-statistic/index.ts b/src/resources/js/features/anime/list-statistic/index.ts new file mode 100644 index 0000000..fa47d56 --- /dev/null +++ b/src/resources/js/features/anime/list-statistic/index.ts @@ -0,0 +1 @@ +export { default as AnimeListStatistic } from './AnimeListStatistic.vue'; diff --git a/src/resources/js/pages/Anime/Show.vue b/src/resources/js/pages/Anime/Show.vue index 9805ea0..de3c03e 100644 --- a/src/resources/js/pages/Anime/Show.vue +++ b/src/resources/js/pages/Anime/Show.vue @@ -4,8 +4,13 @@ import { computed } from 'vue'; import { Head } from '@inertiajs/vue3'; import type { Anime } from '@/entities/anime'; -import { type AnimeListEntry, Status } from '@/entities/anime-list'; +import { + type AnimeListEntry, + type AnimeListStatistics, + Status, +} from '@/entities/anime-list'; import { AnimeListEntryControl } from '@/features/anime/list-entry-control'; +import { AnimeListStatistic } from '@/features/anime/list-statistic'; import { AnimeRating } from '@/features/anime/rating'; import { Badge } from '@/shared/ui/badge'; import { Block } from '@/shared/ui/block'; @@ -15,6 +20,7 @@ import { AuthenticatedLayout } from '@/widgets/layouts'; type Props = { anime: Anime; animeListEntry?: AnimeListEntry; + animeListStatistic: AnimeListStatistics; animeListStatuses: Status[]; }; @@ -89,13 +95,15 @@ const voiceActingList = computed(() => -
- -
+ + +
- Email + Email - Status + Status diff --git a/src/resources/js/shared/ui/hover-card/HoverCard.vue b/src/resources/js/shared/ui/hover-card/HoverCard.vue new file mode 100644 index 0000000..9895b4b --- /dev/null +++ b/src/resources/js/shared/ui/hover-card/HoverCard.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/resources/js/shared/ui/hover-card/HoverCardContent.vue b/src/resources/js/shared/ui/hover-card/HoverCardContent.vue new file mode 100644 index 0000000..1f3f988 --- /dev/null +++ b/src/resources/js/shared/ui/hover-card/HoverCardContent.vue @@ -0,0 +1,44 @@ + + + diff --git a/src/resources/js/shared/ui/hover-card/HoverCardTrigger.vue b/src/resources/js/shared/ui/hover-card/HoverCardTrigger.vue new file mode 100644 index 0000000..25c90cb --- /dev/null +++ b/src/resources/js/shared/ui/hover-card/HoverCardTrigger.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/resources/js/shared/ui/hover-card/index.ts b/src/resources/js/shared/ui/hover-card/index.ts new file mode 100644 index 0000000..dd4a1ef --- /dev/null +++ b/src/resources/js/shared/ui/hover-card/index.ts @@ -0,0 +1,3 @@ +export { default as HoverCard } from './HoverCard.vue'; +export { default as HoverCardContent } from './HoverCardContent.vue'; +export { default as HoverCardTrigger } from './HoverCardTrigger.vue'; diff --git a/src/resources/js/shared/ui/progress/Progress.vue b/src/resources/js/shared/ui/progress/Progress.vue new file mode 100644 index 0000000..60316d4 --- /dev/null +++ b/src/resources/js/shared/ui/progress/Progress.vue @@ -0,0 +1,38 @@ + + + diff --git a/src/resources/js/shared/ui/progress/index.ts b/src/resources/js/shared/ui/progress/index.ts new file mode 100644 index 0000000..63f2d58 --- /dev/null +++ b/src/resources/js/shared/ui/progress/index.ts @@ -0,0 +1 @@ +export { default as Progress } from './Progress.vue';