Skip to content

Commit

Permalink
Added anime list statistic
Browse files Browse the repository at this point in the history
  • Loading branch information
VampireAotD committed Dec 15, 2024
1 parent b8ae417 commit 27f20f1
Show file tree
Hide file tree
Showing 14 changed files with 268 additions and 15 deletions.
10 changes: 7 additions & 3 deletions src/app/Http/Controllers/Anime/AnimeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
);
}

/**
Expand Down
48 changes: 48 additions & 0 deletions src/app/Services/User/UserAnimeListService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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]);
Expand Down
8 changes: 7 additions & 1 deletion src/resources/js/entities/anime-list/model/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export { Status, type AnimeList, type AnimeListEntry } from './types';
export {
Status,
type AnimeList,
type AnimeListEntry,
type AnimeListEntryStatistic,
type AnimeListStatistics,
} from './types';
16 changes: 15 additions & 1 deletion src/resources/js/entities/anime-list/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script setup lang="ts">
import type { AnimeListStatistics } from '@/entities/anime-list';
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/shared/ui/hover-card';
import { Progress } from '@/shared/ui/progress';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/shared/ui/table';
type Props = {
animeListStatistic: AnimeListStatistics;
};
defineProps<Props>();
</script>

<template>
<HoverCard :open-delay="50" :close-delay="0">
<HoverCardTrigger class="cursor-pointer underline decoration-dotted">
Who added this anime?
</HoverCardTrigger>
<HoverCardContent class="w-96" side="bottom" align="start" :side-offset="5">
<Table>
<TableHeader>
<TableRow>
<TableHead>Users</TableHead>
<TableHead>Percentage</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="statistic in animeListStatistic"
:key="statistic.status"
>
<TableCell>
{{ statistic.user_count }}
</TableCell>
<TableCell>
<Progress :model-value="statistic.percentage" />
</TableCell>
<TableCell>
{{ statistic.status }}
</TableCell>
</TableRow>
</TableBody>
</Table>
</HoverCardContent>
</HoverCard>
</template>

<style scoped></style>
1 change: 1 addition & 0 deletions src/resources/js/features/anime/list-statistic/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as AnimeListStatistic } from './AnimeListStatistic.vue';
24 changes: 16 additions & 8 deletions src/resources/js/pages/Anime/Show.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -15,6 +20,7 @@ import { AuthenticatedLayout } from '@/widgets/layouts';
type Props = {
anime: Anime;
animeListEntry?: AnimeListEntry;
animeListStatistic: AnimeListStatistics;
animeListStatuses: Status[];
};
Expand Down Expand Up @@ -89,13 +95,15 @@ const voiceActingList = computed(() =>
</Badge>
</div>

<div>
<AnimeListEntryControl
:anime-id="anime.id"
:entry="animeListEntry"
:statuses="animeListStatuses"
/>
</div>
<AnimeListEntryControl
:anime-id="anime.id"
:entry="animeListEntry"
:statuses="animeListStatuses"
/>

<AnimeListStatistic
:anime-list-statistic="animeListStatistic"
/>

<div>
<ExternalLink
Expand Down
4 changes: 2 additions & 2 deletions src/resources/js/pages/Invitation/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ const toggleInvitationModal = () =>

<TableHeader>
<TableRow>
<TableHead> Email</TableHead>
<TableHead>Email</TableHead>

<TableHead> Status</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>

Expand Down
19 changes: 19 additions & 0 deletions src/resources/js/shared/ui/hover-card/HoverCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script setup lang="ts">
import {
HoverCardRoot,
type HoverCardRootEmits,
type HoverCardRootProps,
useForwardPropsEmits,
} from 'radix-vue';
const props = defineProps<HoverCardRootProps>();
const emits = defineEmits<HoverCardRootEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>

<template>
<HoverCardRoot v-bind="forwarded">
<slot />
</HoverCardRoot>
</template>
44 changes: 44 additions & 0 deletions src/resources/js/shared/ui/hover-card/HoverCardContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue';
import {
HoverCardContent,
type HoverCardContentProps,
HoverCardPortal,
useForwardProps,
} from 'radix-vue';
import { cn } from '@/shared/helpers/tailwind';
const props = withDefaults(
defineProps<HoverCardContentProps & { class?: HTMLAttributes['class'] }>(),
{
sideOffset: 4,
}
);
const delegatedProps = computed(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { class: _, ...delegated } = props;
return delegated;
});
const forwardedProps = useForwardProps(delegatedProps);
</script>

<template>
<HoverCardPortal>
<HoverCardContent
v-bind="forwardedProps"
:class="
cn(
'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
props.class
)
"
>
<slot />
</HoverCardContent>
</HoverCardPortal>
</template>
11 changes: 11 additions & 0 deletions src/resources/js/shared/ui/hover-card/HoverCardTrigger.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup lang="ts">
import { HoverCardTrigger, type HoverCardTriggerProps } from 'radix-vue';
const props = defineProps<HoverCardTriggerProps>();
</script>

<template>
<HoverCardTrigger v-bind="props">
<slot />
</HoverCardTrigger>
</template>
3 changes: 3 additions & 0 deletions src/resources/js/shared/ui/hover-card/index.ts
Original file line number Diff line number Diff line change
@@ -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';
38 changes: 38 additions & 0 deletions src/resources/js/shared/ui/progress/Progress.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { type HTMLAttributes, computed } from 'vue';
import { ProgressIndicator, ProgressRoot, type ProgressRootProps } from 'radix-vue';
import { cn } from '@/shared/helpers/tailwind';
const props = withDefaults(
defineProps<ProgressRootProps & { class?: HTMLAttributes['class'] }>(),
{
modelValue: 0,
}
);
const delegatedProps = computed(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { class: _, ...delegated } = props;
return delegated;
});
</script>

<template>
<ProgressRoot
v-bind="delegatedProps"
:class="
cn(
'relative h-4 w-full overflow-hidden rounded-full bg-secondary',
props.class
)
"
>
<ProgressIndicator
class="h-full w-full flex-1 bg-primary transition-all"
:style="`transform: translateX(-${100 - (props.modelValue ?? 0)}%);`"
/>
</ProgressRoot>
</template>
1 change: 1 addition & 0 deletions src/resources/js/shared/ui/progress/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Progress } from './Progress.vue';

0 comments on commit 27f20f1

Please sign in to comment.