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">
|
<script setup lang="ts">
|
||||||
|
import tinycolor from "tinycolor2";
|
||||||
import type { VNodeRef } from "vue";
|
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);
|
const imageElement = shallowRef<VNodeRef | null>(null);
|
||||||
|
|
||||||
@ -32,6 +34,7 @@ interface Props {
|
|||||||
|
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
};
|
};
|
||||||
|
tooltipCollisionBoundary?: Element;
|
||||||
|
|
||||||
dynamicColor?: boolean;
|
dynamicColor?: boolean;
|
||||||
|
|
||||||
@ -43,12 +46,41 @@ const props = defineProps<Props>();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<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" />
|
<TooltipProvider>
|
||||||
<p class="text-lg line-clamp-2 font-semibold">
|
<Tooltip>
|
||||||
{{ props.title }}
|
<TooltipTrigger as-child>
|
||||||
</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -6,11 +6,18 @@ import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } fro
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const props = withDefaults(defineProps<TooltipContentProps & { class?: HTMLAttributes["class"] }>(), {
|
interface Props {
|
||||||
sideOffset: 4,
|
includeArrow?: boolean
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
arrowClass?: HTMLAttributes["class"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<TooltipContentProps & Props>(), {
|
||||||
|
sideOffset: 4,
|
||||||
|
includeArrow: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const emits = defineEmits<TooltipContentEmits>()
|
const emits = defineEmits<TooltipContentEmits>()
|
||||||
@ -20,15 +27,14 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TooltipPortal>
|
<TooltipPortal>
|
||||||
<TooltipContent
|
<TooltipContent data-slot="tooltip-content" v-bind="{ ...forwarded, ...$attrs }"
|
||||||
data-slot="tooltip-content"
|
: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)">
|
||||||
v-bind="{ ...forwarded, ...$attrs }"
|
<slot />
|
||||||
: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]" />
|
<TooltipArrow
|
||||||
</TooltipContent>
|
:class="cn('bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]', arrowClass)"
|
||||||
</TooltipPortal>
|
v-if="includeArrow" />
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ClientOnly>
|
<TooltipProvider>
|
||||||
<Sonner />
|
<ClientOnly>
|
||||||
</ClientOnly>
|
<Sonner />
|
||||||
<div class="w-full">
|
</ClientOnly>
|
||||||
<header ref="header">
|
<div class="w-full">
|
||||||
<AppHeader />
|
<header ref="header">
|
||||||
</header>
|
<AppHeader />
|
||||||
<main class="h-screen">
|
</header>
|
||||||
<slot />
|
<main class="h-screen">
|
||||||
</main>
|
<slot />
|
||||||
</div>
|
</main>
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -19,7 +21,10 @@ import 'vue-sonner/style.css'
|
|||||||
import Sonner from '@/components/ui/sonner/Sonner.vue';
|
import Sonner from '@/components/ui/sonner/Sonner.vue';
|
||||||
import AppHeader from '@/components/ui/internal/AppHeader.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);
|
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 MediaImageCard from '~/components/ui/internal/MediaImageCard.vue';
|
||||||
import SkeletonMediaImageCard from '~/components/ui/internal/SkeletonMediaImageCard.vue';
|
import SkeletonMediaImageCard from '~/components/ui/internal/SkeletonMediaImageCard.vue';
|
||||||
|
|
||||||
|
const tooltipCollisionBoundary = ref<Element | undefined>(undefined);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -30,28 +31,100 @@ import SkeletonMediaImageCard from '~/components/ui/internal/SkeletonMediaImageC
|
|||||||
View all
|
View all
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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">
|
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"
|
<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"
|
<MediaImageCard image_url="example/okiraku.jpg" title="Okiraku Ryoushu no Tanoshii Ryouchi Bouei"
|
||||||
type="anime" />
|
type="anime" :tooltipEnabled="true" :tooltipMetadata="{
|
||||||
<MediaImageCard image_url="example/oshi no ko.jpg" title="[Oshi no Ko] 3rd Season" type="anime" />
|
studio: 'MAPPA',
|
||||||
<MediaImageCard image_url="example/shibou.jpg" title="Shibou Yuugi de Meshi wo Kuu." type="anime" />
|
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"
|
<MediaImageCard image_url="example/sousounofrieren.jpg" title="Sousou no Frieren 2nd Season"
|
||||||
type="anime" />
|
type="anime" :tooltipEnabled="true" :tooltipMetadata="{
|
||||||
<MediaImageCard image_url="example/tamonkun.jpg" title="Tamon-kun Ima Docchi!?" type="anime" />
|
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"
|
<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"
|
<MediaImageCard image_url="example/jujutsukaisen.jpg"
|
||||||
title="Jujutsu Kaisen: Shimetsu Kaiyuu - Zenpen soetjsot giwjiagjwiag wgi wajig wig gwi"
|
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"
|
<MediaImageCard image_url="example/okiraku.jpg"
|
||||||
title="Okiraku Ryoushu no Tanoshii Ryouchi Bouei teisotsoeitj wgjiwag wgj wigwji"
|
title="Okiraku Ryoushu no Tanoshii Ryouchi Bouei teisotsoeitj wgjiwag wgj wigwji" 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"
|
<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 />
|
<SkeletonMediaImageCard />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user