Skip to content

Commit

Permalink
Refactored code
Browse files Browse the repository at this point in the history
  • Loading branch information
VampireAotD committed Oct 8, 2024
1 parent fb021a3 commit ab009f2
Show file tree
Hide file tree
Showing 13 changed files with 217 additions and 38 deletions.
1 change: 0 additions & 1 deletion src/resources/js/entities/anime-synonym/index.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/resources/js/entities/anime-synonym/model/index.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/resources/js/entities/anime-synonym/model/types.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/resources/js/entities/anime-url/index.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/resources/js/entities/anime-url/model/index.ts

This file was deleted.

7 changes: 0 additions & 7 deletions src/resources/js/entities/anime-url/model/types.ts

This file was deleted.

10 changes: 8 additions & 2 deletions src/resources/js/entities/anime/model/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { type AnimeSynonym } from '@/entities/anime-synonym';
import { type AnimeUrl } from '@/entities/anime-url';
import { type Genre } from '@/entities/genre';
import { type Image } from '@/entities/image';
import { type CountFilter, type RangeFilter } from '@/entities/search';
import { type VoiceActing } from '@/entities/voice-acting';
import { Models } from '@/types';

type AnimeSynonym = Models.Id & {
name: string;
};

type AnimeUrl = Models.Id & {
url: string;
};

type Anime = Models.Id &
Models.Timestamps &
Models.IsDeleted & {
Expand Down
6 changes: 1 addition & 5 deletions src/resources/js/features/anime/rating/AnimeRating.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { mount } from '@vue/test-utils';
import { beforeEach, describe, expect, it } from 'vitest';

import PrimeVue from 'primevue/config';
import Rating from 'primevue/rating';
import { Rating } from '@/shared/ui/rating';

import AnimeRating from './AnimeRating.vue';

Expand All @@ -12,9 +11,6 @@ describe('AnimeRating test (AnimeRating.vue)', () => {
beforeEach(() => {
wrapper = mount(AnimeRating, {
props: { modelValue: 5 },
global: {
plugins: [PrimeVue],
},
});
});

Expand Down
11 changes: 3 additions & 8 deletions src/resources/js/features/anime/rating/AnimeRating.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
<script setup lang="ts">
import Rating from 'primevue/rating';
import { Rating } from '@/shared/ui/rating';
const rating = defineModel<number>();
</script>

<template>
<div class="flex gap-2" role="status" aria-live="polite">
<span id="ratingValue">{{ rating }} / 10</span>
<Rating
v-model="rating"
readonly
:stars="10"
:cancel="false"
aria-labelledby="ratingValue"
/>

<Rating v-model="rating" :max-rating="10" readonly />
</div>
</template>

Expand Down
12 changes: 7 additions & 5 deletions src/resources/js/pages/Anime/Show.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import { AnimeRating } from '@/features/anime/rating';
import { ExternalLink } from '@/shared/ui/external-link';
import { AuthenticatedLayout } from '@/widgets/layouts';
const props = defineProps<{ anime: Anime }>();
type Props = {
anime: Anime;
};
const props = defineProps<Props>();
const rating = computed(() => props.anime.rating);
const links = computed(() => props.anime.urls.map((link) => link.url));
const synonyms = computed(() => props.anime.synonyms.map((synonym) => synonym.name));
const genres = computed((): string =>
props.anime.genres.map((genre) => genre.name).join(', ')
);
const voiceActingList = computed((): string =>
const genres = computed(() => props.anime.genres.map((genre) => genre.name).join(', '));
const voiceActingList = computed(() =>
props.anime.voice_acting.map((voiceActing) => voiceActing.name).join(', ')
);
</script>
Expand Down
123 changes: 123 additions & 0 deletions src/resources/js/shared/ui/rating/Rating.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { mount } from '@vue/test-utils';
import { describe, expect, it } from 'vitest';

import Rating from './Rating.vue';

describe('Rating test (Rating.vue)', () => {
const createWrapper = (props = {}, options = {}) => {
return mount(Rating, {
props: {
maxRating: 5,
modelValue: 0,
...props,
},
global: {
stubs: {
Star: true,
},
},
...options,
});
};

it('Renders the correct number of stars', () => {
const wrapper = createWrapper({ maxRating: 8 });
expect(wrapper.findAll('.relative').length).toBe(8);
});

it('Sets the initial rating correctly', async () => {
const wrapper = createWrapper({ modelValue: 3 });
const filledStars = wrapper.findAll('.absolute');
expect(filledStars[2].attributes('style')).toContain('width: 100%');
expect(filledStars[3].attributes('style')).toContain('width: 0%');
});

it('Updates rating on click', async () => {
const wrapper = createWrapper();
await wrapper.findAll('.relative')[2].trigger('click');
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([3]);
});

it('Handles precision correctly', async () => {
const wrapper = createWrapper({ precision: 0.5 });

await wrapper.vm.handleHover(
{
offsetX: 15,
currentTarget: { offsetWidth: 30 },
},
3
);
await wrapper.vm.$nextTick();
expect(wrapper.vm.hoverRating).toBe(2.5);

await wrapper.vm.handleHover(
{
offsetX: 25,
currentTarget: { offsetWidth: 30 },
},
3
);
await wrapper.vm.$nextTick();
expect(wrapper.vm.hoverRating).toBe(3);

await wrapper.setProps({ precision: 0.1 });
await wrapper.vm.handleHover(
{
offsetX: 13,
currentTarget: { offsetWidth: 30 },
},
3
);
await wrapper.vm.$nextTick();
expect(wrapper.vm.hoverRating).toBe(2.5);

await wrapper.vm.handleHover(
{
offsetX: 17,
currentTarget: { offsetWidth: 30 },
},
3
);
await wrapper.vm.$nextTick();
expect(wrapper.vm.hoverRating).toBe(2.6);
});

it('Handles hover correctly', async () => {
const wrapper = createWrapper();
await wrapper.vm.handleHover(
{
offsetX: 15,
currentTarget: { offsetWidth: 30 },
},
3
);
await wrapper.vm.$nextTick();

const filledStars = wrapper.findAll('.absolute');
expect(filledStars[2].attributes('style')).toContain('width: 100%');
expect(filledStars[3].attributes('style')).toContain('width: 0%');
expect(wrapper.vm.hoverRating).toBe(3);
});

it('Respects readonly prop', async () => {
const wrapper = createWrapper({ readonly: true });
await wrapper.findAll('.relative')[2].trigger('click');
expect(wrapper.emitted('update:modelValue')).toBeFalsy();
});

it('Uses custom icons when provided', () => {
const wrapper = createWrapper(
{},
{
slots: {
'empty-icon': '<span class="empty-custom-icon"></span>',
'filled-icon': '<span class="filled-custom-icon"></span>',
},
}
);

expect(wrapper.find('.empty-custom-icon').exists()).toBeTruthy();
expect(wrapper.find('.filled-custom-icon').exists()).toBeTruthy();
});
});
74 changes: 74 additions & 0 deletions src/resources/js/shared/ui/rating/Rating.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Star } from 'lucide-vue-next';
type Props = {
maxRating?: number;
precision?: number;
readonly?: boolean;
};
const rating = defineModel<number>();
const { maxRating = 5, precision = 1, readonly = false } = defineProps<Props>();
const hoverRating = ref<number | null>(null);
const setRating = (value: number) => {
if (readonly) return;
rating.value = value;
};
const handleHover = (event: MouseEvent, iconIndex: number) => {
if (readonly) return;
const { offsetX, currentTarget } = event;
const targetWidth = (currentTarget as HTMLElement).offsetWidth;
const hoverValue =
iconIndex - 1 + Math.ceil(offsetX / targetWidth / precision) * precision;
hoverRating.value = parseFloat(hoverValue.toFixed(1));
};
const resetHoverRating = () => {
if (readonly) return;
hoverRating.value = null;
};
const iconFill = (iconIndex: number) => {
const currentRating = hoverRating.value !== null ? hoverRating.value : rating.value;
return Math.min(Math.max(currentRating - (iconIndex - 1), 0), 1) * 100;
};
</script>

<template>
<div class="flex space-x-1" @mouseleave="resetHoverRating">
<div
v-for="i in maxRating"
:key="i"
class="relative cursor-pointer"
@mousemove="handleHover($event, i)"
@click="setRating(i)"
>
<slot name="empty-icon">
<Star class="w-6 h-6 text-gray-400" fill="currentColor" stroke="none" />
</slot>

<div
class="absolute top-0 left-0 w-6 h-6 overflow-hidden"
:style="{ width: `${iconFill(i)}%` }"
>
<slot name="filled-icon">
<Star
class="w-6 h-6 text-yellow-400"
fill="currentColor"
stroke="none"
/>
</slot>
</div>
</div>
</div>
</template>

<style scoped></style>
1 change: 1 addition & 0 deletions src/resources/js/shared/ui/rating/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as Rating } from './Rating.vue';

0 comments on commit ab009f2

Please sign in to comment.