Basic implementation for anime page

This commit is contained in:
2026-01-18 00:46:32 +05:00
parent da573b6b33
commit 7390c631d5
31 changed files with 846 additions and 162 deletions

View File

@ -7,20 +7,19 @@ import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
import { badgeVariants } from "."
const props = defineProps<PrimitiveProps & {
variant?: BadgeVariants["variant"]
class?: HTMLAttributes["class"]
}>()
const props = withDefaults(defineProps<PrimitiveProps & {
variant?: BadgeVariants["variant"]
size?: BadgeVariants["size"]
class?: HTMLAttributes["class"]
}>(), {
size: "default"
});
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<Primitive
data-slot="badge"
:class="cn(badgeVariants({ variant }), props.class)"
v-bind="delegatedProps"
>
<slot />
</Primitive>
<Primitive data-slot="badge" :class="cn(badgeVariants({ variant, size }), props.class)" v-bind="delegatedProps">
<slot />
</Primitive>
</template>

View File

@ -4,23 +4,28 @@ 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",
},
"inline-flex items-center justify-center rounded-full border 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",
},
size: {
default: "px-2 py-0.5",
xs: "px-1 py-0.2",
lg: "px-3 py-1"
}
},
defaultVariants: {
variant: "default",
},
},
defaultVariants: {
variant: "default",
},
},
)
export type BadgeVariants = VariantProps<typeof badgeVariants>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="card"
:class="
cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="card-action"
:class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="card-content"
:class="cn('px-6', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<p
data-slot="card-description"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</p>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="card-footer"
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="card-header"
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<h3
data-slot="card-title"
:class="cn('leading-none font-semibold', props.class)"
>
<slot />
</h3>
</template>

View File

@ -0,0 +1,7 @@
export { default as Card } from "./Card.vue"
export { default as CardAction } from "./CardAction.vue"
export { default as CardContent } from "./CardContent.vue"
export { default as CardDescription } from "./CardDescription.vue"
export { default as CardFooter } from "./CardFooter.vue"
export { default as CardHeader } from "./CardHeader.vue"
export { default as CardTitle } from "./CardTitle.vue"

View File

@ -0,0 +1,82 @@
<script setup lang="ts" generic="Provider extends keyof ConfiguredImageProviders = ProviderDefaults['provider']">
import { Tooltip, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from "@/lib/utils";
import type { ConfiguredImageProviders, ProviderDefaults } from '@nuxt/image';
import type { HTMLAttributes } from 'vue';
import { imageVariants } from '.';
interface Props {
src: string;
alt?: string;
title?: string;
sizes?: string;
width?: number;
height?: number;
loading?: 'lazy' | 'eager';
provider?: Provider;
quality?: number;
class?: HTMLAttributes['class'];
overlayHeight?: string;
overlayOpacity?: number;
overlayClass?: string;
imageClass?: string;
variant?: 'default' | 'rounded' | 'elevated' | 'minimal';
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
}
const props = withDefaults(defineProps<Props>(), {
alt: '',
title: '',
sizes: '100vw sm:50vw md:33vw lg:25vw',
width: 400,
height: 300,
loading: 'lazy',
provider: undefined,
quality: 80,
overlayHeight: '4em',
overlayOpacity: 1,
overlayClass: '',
imageClass: '',
variant: 'default',
size: 'md',
});
const emit = defineEmits<{
hover: [state: boolean];
click: [event: MouseEvent];
}>();
const handleHover = (state: boolean) => {
emit('hover', state);
};
const handleClick = (event: MouseEvent) => {
emit('click', event);
};
</script>
<template>
<Tooltip>
<TooltipTrigger as-child>
<div :class="cn(imageVariants({ variant, size }), props.class)" @mouseenter="handleHover(true)"
@mouseleave="handleHover(false)" @click="handleClick">
<NuxtImg :src="src" :alt="alt" :sizes="sizes" :width="width" :height="height" :loading="loading"
:provider="provider" :quality="quality" :class="cn('w-full h-auto object-cover', imageClass)" />
<div
:class="cn('absolute bottom-0 left-0 right-0 overlay-gradient bg-linear-to-t group-hover:opacity-0 opacity-100 from-muted from-20% to-transparent flex items-end transition-opacity p-4', overlayClass)">
<span v-if="title" class="text-white font-semibold text-sm md:text-base line-clamp-2">
{{ title }}
</span>
</div>
</div>
</TooltipTrigger>
<slot />
</Tooltip>
</template>
<style scoped>
.overlay-gradient {
height: v-bind(overlayHeight);
}
</style>

View File

@ -0,0 +1,31 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as HoverableImage } from "./HoverableImage.vue"
export const imageVariants = cva(
"relative group cursor-pointer overflow-hidden rounded-lg",
{
variants: {
variant: {
default: "border border-border",
rounded: "rounded-full overflow-hidden",
elevated: "shadow-lg hover:shadow-xl",
minimal: "border-0",
},
size: {
sm: "max-w-xs",
md: "max-w-md",
lg: "max-w-lg",
xl: "max-w-xl",
full: "w-full",
}
},
defaultVariants: {
variant: "default",
size: "md",
},
}
)
export type HoverableImageVariants = VariantProps<typeof imageVariants>

View File

@ -17,10 +17,11 @@ watch(palette, () => {
});
interface Props {
id: number;
image_url: string;
image_alt?: string;
title: string;
type: 'anime' | 'manga' | 'character' | 'voice_actor';
type: string;
subtitle?: string;
description?: string;
@ -46,45 +47,46 @@ const props = defineProps<Props>();
</script>
<template>
<div class="h-full overflow-hidden max-w-64">
<div class="flex flex-col gap-2 cursor-pointer text-muted-foreground vibrant-color">
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<div>
<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>
</div>
</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>
<NuxtLink :to="`anime/${id}`">
<div class="h-full overflow-hidden max-w-64">
<div class="flex flex-col gap-2 cursor-pointer text-muted-foreground vibrant-color">
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<div>
<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 max-w-[12ch] 2xl:max-w-full">
{{ 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>
</TooltipTrigger>
<TooltipContent :sideOffset="16" :collisionBoundary="props.tooltipCollisionBoundary"
:avoidCollisions="true" side="right" tooltipColor="muted" tooltipArrowColor="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>
<div class="flex gap-2">
<Badge v-for="tag in props.tooltipMetadata?.tags">
{{ tag }}
</Badge>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
</div>
</NuxtLink>
</template>
<style scoped>

View File

@ -9,7 +9,7 @@ const props = defineProps<Props>();
</script>
<template>
<div :class="cn('rounded-lg h-full overflow-hidden w-full max-w-64', props.class)">
<div :class="cn('rounded-lg h-full min-h-72 overflow-hidden w-full max-w-64', props.class)">
<div class="flex flex-col gap-2 h-full pb-6">
<Skeleton class="h-full w-full" />
<Skeleton class="h-6 w-3/4" />

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { TabsRootEmits, TabsRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TabsRoot, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<TabsRootProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<TabsRootEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<TabsRoot
v-slot="slotProps"
data-slot="tabs"
v-bind="forwarded"
:class="cn('flex flex-col gap-2', props.class)"
>
<slot v-bind="slotProps" />
</TabsRoot>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import type { TabsContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TabsContent } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<TabsContentProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<TabsContent
data-slot="tabs-content"
:class="cn('flex-1 outline-none', props.class)"
v-bind="delegatedProps"
>
<slot />
</TabsContent>
</template>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { TabsListProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TabsList } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<TabsListProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<TabsList
data-slot="tabs-list"
v-bind="delegatedProps"
:class="cn(
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
props.class,
)"
>
<slot />
</TabsList>
</template>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import type { TabsTriggerProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TabsTrigger, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<TabsTriggerProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<TabsTrigger
data-slot="tabs-trigger"
:class="cn(
'data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
props.class,
)"
v-bind="forwardedProps"
>
<slot />
</TabsTrigger>
</template>

View File

@ -0,0 +1,4 @@
export { default as Tabs } from "./Tabs.vue"
export { default as TabsContent } from "./TabsContent.vue"
export { default as TabsList } from "./TabsList.vue"
export { default as TabsTrigger } from "./TabsTrigger.vue"

View File

@ -4,6 +4,7 @@ import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { TooltipArrow, TooltipContent, TooltipPortal, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/lib/utils"
import { tooltipArrowVariants, tooltipContentVariants, type TooltipArrowVariants, type TooltipContentVariants } from "."
defineOptions({
inheritAttrs: false,
@ -13,6 +14,8 @@ interface Props {
includeArrow?: boolean
class?: HTMLAttributes["class"]
arrowClass?: HTMLAttributes["class"]
tooltipColor?: TooltipContentVariants["color"]
tooltipArrowColor?: TooltipArrowVariants["color"]
}
const props = withDefaults(defineProps<TooltipContentProps & Props>(), {
@ -29,11 +32,11 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<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)">
:class="cn(tooltipContentVariants({ variant: 'default', color: tooltipColor }), props.class)">
<slot />
<TooltipArrow
:class="cn('bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]', arrowClass)"
:class="cn(tooltipArrowVariants({ variant: 'default', color: tooltipArrowColor }), arrowClass)"
v-if="includeArrow" />
</TooltipContent>
</TooltipPortal>

View File

@ -2,3 +2,48 @@ export { default as Tooltip } from "./Tooltip.vue"
export { default as TooltipContent } from "./TooltipContent.vue"
export { default as TooltipProvider } from "./TooltipProvider.vue"
export { default as TooltipTrigger } from "./TooltipTrigger.vue"
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export const tooltipContentVariants = cva(
"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",
{
variants: {
variant: {
default: ""
},
color: {
default: "bg-foreground",
muted: "bg-muted text-muted-foreground"
}
},
defaultVariants: {
variant: "default",
color: "default",
},
}
)
export const tooltipArrowVariants = cva(
"z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]",
{
variants: {
variant: {
default: ""
},
color: {
default: "bg-foreground fill-foreground",
muted: "bg-muted fill-muted"
}
},
defaultVariants: {
variant: "default",
color: "default"
}
},
)
export type TooltipContentVariants = VariantProps<typeof tooltipContentVariants>
export type TooltipArrowVariants = VariantProps<typeof tooltipArrowVariants>