mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
chore(book): unify rating star colors across views
This commit is contained in:
@@ -28,6 +28,7 @@ import { COVER_ASPECT_RATIO_KEY, DEFAULT_COVER_ASPECT_RATIO } from '../lib/cover
|
||||
import { useRefreshMetadata } from '../composables/useRefreshMetadata'
|
||||
import { usePermissions } from '@/features/auth/composables/usePermissions'
|
||||
import SendBookDialog from '@/features/email/components/SendBookDialog.vue'
|
||||
import { RATING_STARS, getRatingStarClass } from '@/features/book/lib/rating-stars'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -197,10 +198,10 @@ function openAuthorBrowse() {
|
||||
<div v-if="!selectionMode" class="flex items-center gap-1.5 shrink-0" @click.stop>
|
||||
<!-- Star rating -->
|
||||
<div class="hidden sm:flex items-center gap-0.5" @mouseleave="hoverRating = null">
|
||||
<Tooltip v-for="star in 5" :key="star">
|
||||
<Tooltip v-for="star in RATING_STARS" :key="star">
|
||||
<TooltipTrigger as-child>
|
||||
<button class="p-0.5 transition-colors" @mouseenter="hoverRating = star" @click="setRating(star)">
|
||||
<Star class="size-3" :class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'" />
|
||||
<Star class="size-3" :class="getRatingStarClass(star, displayRating)" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Rate {{ star }}</TooltipContent>
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('BookListRow — present state', () => {
|
||||
it('syncs the displayed star rating when the book prop changes externally', async () => {
|
||||
const wrapper = mount(BookListRow, { props: { book: presentBook }, global: globalStubs })
|
||||
|
||||
expect(wrapper.findAll('.fill-amber-400')).toHaveLength(0)
|
||||
expect(wrapper.findAll('.fill-lime-400')).toHaveLength(0)
|
||||
|
||||
await wrapper.setProps({
|
||||
book: {
|
||||
@@ -143,6 +143,6 @@ describe('BookListRow — present state', () => {
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('.fill-amber-400')).toHaveLength(4)
|
||||
expect(wrapper.findAll('.fill-lime-400')).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,6 +29,7 @@ import MetadataScoreBreakdown from '@/features/metadata-score/components/Metadat
|
||||
import { useMetadataScoreWeights } from '@/features/metadata-score/composables/useMetadataScoreWeights'
|
||||
import { useSafeHtml } from '@/features/book/composables/useSafeHtml'
|
||||
import { useKoreaderBookProgress } from '@/features/koreader/composables/useKoreaderBookProgress'
|
||||
import { RATING_STARS, getRatingStarClass } from '@/features/book/lib/rating-stars'
|
||||
|
||||
type FileProgress = {
|
||||
percentage: number
|
||||
@@ -323,7 +324,7 @@ async function setRating(star: number) {
|
||||
}
|
||||
}
|
||||
|
||||
const ratingStars = [1, 2, 3, 4, 5]
|
||||
const ratingStars = RATING_STARS
|
||||
|
||||
const { setStatus } = useBookStatus()
|
||||
|
||||
@@ -794,19 +795,14 @@ watch(
|
||||
@mouseenter="hoverRating = star"
|
||||
@click="setRating(star)"
|
||||
>
|
||||
<Star class="size-4" :class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'" />
|
||||
<Star class="size-4" :class="getRatingStarClass(star, displayRating)" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ isRatingLocked ? 'Rating is locked' : `Rate ${star}` }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Star
|
||||
v-for="star in ratingStars"
|
||||
:key="star"
|
||||
class="size-4"
|
||||
:class="(localRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'"
|
||||
/>
|
||||
<Star v-for="star in ratingStars" :key="star" class="size-4" :class="getRatingStarClass(star, localRating)" />
|
||||
</template>
|
||||
</div>
|
||||
<template v-if="isRatingLocked">
|
||||
@@ -1156,19 +1152,14 @@ watch(
|
||||
@mouseenter="hoverRating = star"
|
||||
@click="setRating(star)"
|
||||
>
|
||||
<Star class="size-3.5" :class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'" />
|
||||
<Star class="size-3.5" :class="getRatingStarClass(star, displayRating)" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ isRatingLocked ? 'Rating is locked' : `Rate ${star}` }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Star
|
||||
v-for="star in ratingStars"
|
||||
:key="star"
|
||||
class="size-3.5"
|
||||
:class="(localRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'"
|
||||
/>
|
||||
<Star v-for="star in ratingStars" :key="star" class="size-3.5" :class="getRatingStarClass(star, localRating)" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useNarratorSearch } from '../../../composables/useNarratorSearch'
|
||||
import { useGenreSearch, useTagSearch } from '../../../composables/useTagSearch'
|
||||
import { usePublisherSearch, useSeriesNameSearch, useLanguageSearch } from '../../../composables/useMetadataFieldSearch'
|
||||
import InputWithSuggestions from '@/components/ui/InputWithSuggestions.vue'
|
||||
import { RATING_STARS, getRatingStarClass } from '@/features/book/lib/rating-stars'
|
||||
|
||||
const props = defineProps<{ book: BookDetail }>()
|
||||
const emit = defineEmits<{
|
||||
@@ -581,7 +582,7 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
|
||||
:class="isLocked('rating') ? 'opacity-50 cursor-not-allowed' : ''"
|
||||
@mouseleave="hoverRating = null"
|
||||
>
|
||||
<Tooltip v-for="star in 5" :key="star">
|
||||
<Tooltip v-for="star in RATING_STARS" :key="star">
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
type="button"
|
||||
@@ -590,10 +591,7 @@ function handleCoverChanged(source: 'extracted' | 'custom' | null) {
|
||||
@mouseenter="hoverRating = star"
|
||||
@click="setRating(star)"
|
||||
>
|
||||
<Star
|
||||
class="size-5 sm:size-4"
|
||||
:class="(displayRating ?? 0) >= star ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/60'"
|
||||
/>
|
||||
<Star class="size-5 sm:size-4" :class="getRatingStarClass(star, displayRating)" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Rate {{ star }}</TooltipContent>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Star } from 'lucide-vue-next'
|
||||
import { RATING_STARS, getRatingStarClass } from '@/features/book/lib/rating-stars'
|
||||
|
||||
const props = defineProps<{
|
||||
value: number | null
|
||||
@@ -11,7 +12,7 @@ const emit = defineEmits<{
|
||||
save: [value: number | null]
|
||||
}>()
|
||||
|
||||
const STARS = [1, 2, 3, 4, 5]
|
||||
const STARS = RATING_STARS
|
||||
const groupRef = ref<HTMLDivElement | null>(null)
|
||||
|
||||
function getStarFromEvent(event: Event): number | null {
|
||||
@@ -73,10 +74,7 @@ function getTabIndex(star: number): number {
|
||||
:aria-pressed="star <= (value ?? 0)"
|
||||
type="button"
|
||||
class="transition-colors"
|
||||
:class="[
|
||||
isReadOnly ? 'cursor-default' : 'cursor-pointer hover:scale-110',
|
||||
star <= (value ?? 0) ? 'text-amber-400' : 'text-muted-foreground/30',
|
||||
]"
|
||||
:class="[isReadOnly ? 'cursor-default' : 'cursor-pointer hover:scale-110', getRatingStarClass(star, value, 'text-muted-foreground/30')]"
|
||||
@click="handleStarClick"
|
||||
@keydown="handleStarKeydown"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export const RATING_STARS = [1, 2, 3, 4, 5] as const
|
||||
|
||||
export type RatingStarValue = (typeof RATING_STARS)[number]
|
||||
|
||||
export const RATING_VALUE_COLOR_CLASSES: Record<RatingStarValue, string> = {
|
||||
1: 'fill-red-500 text-red-500',
|
||||
2: 'fill-orange-400 text-orange-400',
|
||||
3: 'fill-yellow-400 text-yellow-400',
|
||||
4: 'fill-lime-400 text-lime-400',
|
||||
5: 'fill-green-600 text-green-600',
|
||||
}
|
||||
|
||||
function normalizeRatingValue(rating: number | null | undefined): RatingStarValue | null {
|
||||
if (rating == null) return null
|
||||
const rounded = Math.round(rating)
|
||||
if (rounded < 1 || rounded > 5) return null
|
||||
return rounded as RatingStarValue
|
||||
}
|
||||
|
||||
export function getRatingFilledClass(rating: number | null | undefined): string {
|
||||
const value = normalizeRatingValue(rating)
|
||||
return value ? RATING_VALUE_COLOR_CLASSES[value] : 'fill-amber-400 text-amber-400'
|
||||
}
|
||||
|
||||
export function getRatingStarClass(star: number, rating: number | null | undefined, emptyClass = 'text-muted-foreground/60'): string {
|
||||
if ((rating ?? 0) < star) return emptyClass
|
||||
return getRatingFilledClass(rating)
|
||||
}
|
||||
@@ -36,8 +36,8 @@ export const themes: Theme[] = [
|
||||
{
|
||||
name: 'crimson',
|
||||
label: 'Crimson',
|
||||
light: { fg: '#212529', bg: '#ffffff', link: '#dd0031' },
|
||||
dark: { fg: '#dee2e6', bg: '#343a40', link: '#ff4081' },
|
||||
light: { fg: '#2f1f25', bg: '#fdf1f4', link: '#dd0031' },
|
||||
dark: { fg: '#f3dbe2', bg: '#3a252d', link: '#ff5a86' },
|
||||
},
|
||||
{
|
||||
name: 'meadow',
|
||||
|
||||
Reference in New Issue
Block a user