Add tooltip content for MediaImageCard
This commit is contained in:
26
app/components/ui/badge/Badge.vue
Normal file
26
app/components/ui/badge/Badge.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { BadgeVariants } from "."
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Primitive } from "reka-ui"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { badgeVariants } from "."
|
||||
|
||||
const props = defineProps<PrimitiveProps & {
|
||||
variant?: BadgeVariants["variant"]
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="badge"
|
||||
:class="cn(badgeVariants({ variant }), props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
26
app/components/ui/badge/index.ts
Normal file
26
app/components/ui/badge/index.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import { cva } from "class-variance-authority"
|
||||
|
||||
export { default as Badge } from "./Badge.vue"
|
||||
|
||||
export const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
export type BadgeVariants = VariantProps<typeof badgeVariants>
|
||||
@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import tinycolor from "tinycolor2";
|
||||
import type { VNodeRef } from "vue";
|
||||
import tinycolor from "tinycolor2"
|
||||
import Badge from "../badge/Badge.vue";
|
||||
import { Dot } from "lucide-vue-next";
|
||||
|
||||
const imageElement = shallowRef<VNodeRef | null>(null);
|
||||
|
||||
@ -32,6 +34,7 @@ interface Props {
|
||||
|
||||
tags?: string[];
|
||||
};
|
||||
tooltipCollisionBoundary?: Element;
|
||||
|
||||
dynamicColor?: boolean;
|
||||
|
||||
@ -43,12 +46,41 @@ const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rounded-lg h-full overflow-hidden max-w-64">
|
||||
<div class="h-full overflow-hidden max-w-64">
|
||||
<div class="flex flex-col gap-2 cursor-pointer text-muted-foreground vibrant-color">
|
||||
<NuxtImg ref="imageElement" :src="props.image_url" sizes="128px md:256px" quality="100" />
|
||||
<p class="text-lg line-clamp-2 font-semibold">
|
||||
{{ props.title }}
|
||||
</p>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<NuxtImg class="rounded-lg" ref="imageElement" :src="props.image_url" sizes="128px md:256px"
|
||||
quality="100" />
|
||||
<p class="text-lg line-clamp-2 font-semibold">
|
||||
{{ props.title }}
|
||||
</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent :sideOffset="16" :collisionBoundary="props.tooltipCollisionBoundary"
|
||||
:avoidCollisions="true" side="right" class="bg-muted text-muted-foreground"
|
||||
arrowClass="bg-muted fill-muted">
|
||||
<div class="flex flex-col gap-1 m-4">
|
||||
<div class="flex">
|
||||
<p class="text-xl font-bold truncate max-w-[24ch]">{{ props.title }}</p>
|
||||
</div>
|
||||
<p class="text-lg font-semibold tracking-wider" :style="{ color: paletteVibrantHsl }">{{
|
||||
props.tooltipMetadata?.studio }}
|
||||
</p>
|
||||
<div class="flex items-center text-base font-semibold">
|
||||
<p>{{ props.tooltipMetadata?.type }}</p>
|
||||
<Dot />
|
||||
<p>{{ props.tooltipMetadata?.rating }} episodes</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Badge v-for="tag in props.tooltipMetadata?.tags">
|
||||
{{ tag }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -6,11 +6,18 @@ import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } fro
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes["class"] }>(), {
|
||||
sideOffset: 4,
|
||||
interface Props {
|
||||
includeArrow?: boolean
|
||||
class?: HTMLAttributes["class"]
|
||||
arrowClass?: HTMLAttributes["class"]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TooltipContentProps & Props>(), {
|
||||
sideOffset: 4,
|
||||
includeArrow: true
|
||||
})
|
||||
|
||||
const emits = defineEmits<TooltipContentEmits>()
|
||||
@ -20,15 +27,14 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipPortal>
|
||||
<TooltipContent
|
||||
data-slot="tooltip-content"
|
||||
v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="cn('bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance', props.class)"
|
||||
>
|
||||
<slot />
|
||||
<TooltipPortal>
|
||||
<TooltipContent data-slot="tooltip-content" v-bind="{ ...forwarded, ...$attrs }"
|
||||
:class="cn('bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance', props.class)">
|
||||
<slot />
|
||||
|
||||
<TooltipArrow class="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
<TooltipArrow
|
||||
:class="cn('bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]', arrowClass)"
|
||||
v-if="includeArrow" />
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</template>
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
<template>
|
||||
<ThemeProvider>
|
||||
<ClientOnly>
|
||||
<Sonner />
|
||||
</ClientOnly>
|
||||
<div class="w-full">
|
||||
<header ref="header">
|
||||
<AppHeader />
|
||||
</header>
|
||||
<main class="h-screen">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
<TooltipProvider>
|
||||
<ClientOnly>
|
||||
<Sonner />
|
||||
</ClientOnly>
|
||||
<div class="w-full">
|
||||
<header ref="header">
|
||||
<AppHeader />
|
||||
</header>
|
||||
<main class="h-screen">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</ThemeProvider>
|
||||
</template>
|
||||
|
||||
@ -19,7 +21,10 @@ import 'vue-sonner/style.css'
|
||||
import Sonner from '@/components/ui/sonner/Sonner.vue';
|
||||
import AppHeader from '@/components/ui/internal/AppHeader.vue';
|
||||
|
||||
import Headroom from "headroom.js"; import ThemeProvider from '~/components/ui/provider/ThemeProvider.vue';
|
||||
import Headroom from "headroom.js";
|
||||
import ThemeProvider from '~/components/ui/provider/ThemeProvider.vue';
|
||||
import TooltipProvider from '~/components/ui/tooltip/TooltipProvider.vue';
|
||||
|
||||
|
||||
|
||||
const header = ref(null);
|
||||
|
||||
@ -4,6 +4,7 @@ import HeroSection from '@/components/ui/internal/HeroSection.vue';
|
||||
import MediaImageCard from '~/components/ui/internal/MediaImageCard.vue';
|
||||
import SkeletonMediaImageCard from '~/components/ui/internal/SkeletonMediaImageCard.vue';
|
||||
|
||||
const tooltipCollisionBoundary = ref<Element | undefined>(undefined);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -30,28 +31,100 @@ import SkeletonMediaImageCard from '~/components/ui/internal/SkeletonMediaImageC
|
||||
View all
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
<div ref="tooltipCollisionBoundary"
|
||||
class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 2xl:grid-cols-6 gap-4 gap-y-12 justify-items-center w-full">
|
||||
<MediaImageCard image_url="example/jigokuraku.jpg" title="Jigokuraku 2nd Season" type="anime" />
|
||||
<MediaImageCard image_url="example/jigokuraku.jpg" title="Jigokuraku 2nd Season" type="anime"
|
||||
:tooltipEnabled="true" :tooltipMetadata="{
|
||||
studio: 'MAPPA',
|
||||
type: 'TV Show',
|
||||
episodes: 12,
|
||||
rating: 78,
|
||||
tags: ['action', 'adventure']
|
||||
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
|
||||
<MediaImageCard image_url="example/jujutsukaisen.jpg"
|
||||
title="Jujutsu Kaisen: Shimetsu Kaiyuu - Zenpen" type="anime" />
|
||||
title="Jujutsu Kaisen: Shimetsu Kaiyuu - Zenpen" type="anime" :tooltipEnabled="true"
|
||||
:tooltipMetadata="{
|
||||
studio: 'MAPPA',
|
||||
type: 'TV Show',
|
||||
episodes: 12,
|
||||
rating: 78,
|
||||
tags: ['action', 'adventure']
|
||||
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
|
||||
<MediaImageCard image_url="example/okiraku.jpg" title="Okiraku Ryoushu no Tanoshii Ryouchi Bouei"
|
||||
type="anime" />
|
||||
<MediaImageCard image_url="example/oshi no ko.jpg" title="[Oshi no Ko] 3rd Season" type="anime" />
|
||||
<MediaImageCard image_url="example/shibou.jpg" title="Shibou Yuugi de Meshi wo Kuu." type="anime" />
|
||||
type="anime" :tooltipEnabled="true" :tooltipMetadata="{
|
||||
studio: 'MAPPA',
|
||||
type: 'TV Show',
|
||||
episodes: 12,
|
||||
rating: 78,
|
||||
tags: ['action', 'adventure']
|
||||
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
|
||||
<MediaImageCard image_url="example/oshi no ko.jpg" title="[Oshi no Ko] 3rd Season" type="anime"
|
||||
:tooltipEnabled="true" :tooltipMetadata="{
|
||||
studio: 'MAPPA',
|
||||
type: 'TV Show',
|
||||
episodes: 12,
|
||||
rating: 78,
|
||||
tags: ['action', 'adventure']
|
||||
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
|
||||
<MediaImageCard image_url="example/shibou.jpg" title="Shibou Yuugi de Meshi wo Kuu." type="anime"
|
||||
:tooltipEnabled="true" :tooltipMetadata="{
|
||||
studio: 'MAPPA',
|
||||
type: 'TV Show',
|
||||
episodes: 12,
|
||||
rating: 78,
|
||||
tags: ['action', 'adventure']
|
||||
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
|
||||
<MediaImageCard image_url="example/sousounofrieren.jpg" title="Sousou no Frieren 2nd Season"
|
||||
type="anime" />
|
||||
<MediaImageCard image_url="example/tamonkun.jpg" title="Tamon-kun Ima Docchi!?" type="anime" />
|
||||
type="anime" :tooltipEnabled="true" :tooltipMetadata="{
|
||||
studio: 'MAPPA',
|
||||
type: 'TV Show',
|
||||
episodes: 12,
|
||||
rating: 78,
|
||||
tags: ['action', 'adventure']
|
||||
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
|
||||
<MediaImageCard image_url="example/tamonkun.jpg" title="Tamon-kun Ima Docchi!?" type="anime"
|
||||
:tooltipEnabled="true" :tooltipMetadata="{
|
||||
studio: 'MAPPA',
|
||||
type: 'TV Show',
|
||||
episodes: 12,
|
||||
rating: 78,
|
||||
tags: ['action', 'adventure']
|
||||
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
|
||||
<MediaImageCard image_url="example/yuusha party.jpg" title="Yuusha Party wo Oidasareta Kiyou Binbou"
|
||||
type="anime" />
|
||||
type="anime" :tooltipEnabled="true" :tooltipMetadata="{
|
||||
studio: 'MAPPA',
|
||||
type: 'TV Show',
|
||||
episodes: 12,
|
||||
rating: 78,
|
||||
tags: ['action', 'adventure']
|
||||
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
|
||||
<MediaImageCard image_url="example/jujutsukaisen.jpg"
|
||||
title="Jujutsu Kaisen: Shimetsu Kaiyuu - Zenpen soetjsot giwjiagjwiag wgi wajig wig gwi"
|
||||
type="anime" />
|
||||
type="anime" :tooltipEnabled="true" :tooltipMetadata="{
|
||||
studio: 'MAPPA',
|
||||
type: 'TV Show',
|
||||
episodes: 12,
|
||||
rating: 78,
|
||||
tags: ['action', 'adventure']
|
||||
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
|
||||
<MediaImageCard image_url="example/okiraku.jpg"
|
||||
title="Okiraku Ryoushu no Tanoshii Ryouchi Bouei teisotsoeitj wgjiwag wgj wigwji"
|
||||
type="anime" />
|
||||
title="Okiraku Ryoushu no Tanoshii Ryouchi Bouei teisotsoeitj wgjiwag wgj wigwji" type="anime"
|
||||
:tooltipEnabled="true" :tooltipMetadata="{
|
||||
studio: 'MAPPA',
|
||||
type: 'TV Show',
|
||||
episodes: 12,
|
||||
rating: 78,
|
||||
tags: ['action', 'adventure']
|
||||
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
|
||||
<MediaImageCard image_url="example/oshi no ko.jpg"
|
||||
title="[Oshi no Ko] 3rd Season iaowjegiogjoiawgio waiogjawiog wag" type="anime" />
|
||||
title="[Oshi no Ko] 3rd Season iaowjegiogjoiawgio waiogjawiog wag" type="anime"
|
||||
:tooltipEnabled="true" :tooltipMetadata="{
|
||||
studio: 'MAPPA',
|
||||
type: 'TV Show',
|
||||
episodes: 12,
|
||||
rating: 78,
|
||||
tags: ['action', 'adventure']
|
||||
}" :tooltipCollisionBoundary="tooltipCollisionBoundary" />
|
||||
<SkeletonMediaImageCard />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user