Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/llms-txt-hub / apps/web/components/ui/favorite-button.tsx |
| Date | Not specified in source |
| Repository | Janady13/llms-txt-hub |
| Revision | e0f4bf978d4df168369c7601d9c5f5ea89e84377 |
| Branch | main |
| Path or archive member | apps/web/components/ui/favorite-button.tsx |
| Original SHA-256 | 6190280c0ef605f7c706a442946cb7d4c7772cf25c6a726da5f9e5d8081b6e9c |
| Git blob SHA | 64077fb9caf213c0cff5632979f8115eac302125 |
| Reading-copy SHA-256 | 6190280c0ef605f7c706a442946cb7d4c7772cf25c6a726da5f9e5d8081b6e9c |
Recovered source transcript
Download complete source text ↓110 lines · 3.1 KB
'use client'
import { useAuth } from '@thedaviddias/auth'
import { cn } from '@thedaviddias/design-system/lib/utils'
import { Heart } from 'lucide-react'
import { useState } from 'react'
import { useFavorites } from '@/contexts/favorites-context'
interface FavoriteButtonProps {
slug: string
className?: string
size?: 'sm' | 'md' | 'lg'
variant?: 'default' | 'ghost'
}
/**
* Favorite button component with heart icon and animation
* @param props - Component props
* @param props.slug - Unique identifier for the item to favorite
* @param props.className - Additional CSS classes
* @param props.size - Button size variant
* @param props.variant - Visual style variant
* @returns A button to toggle favorite status
*/
export function FavoriteButton({
slug,
className,
size = 'md',
variant = 'default'
}: FavoriteButtonProps) {
const { isFavorite, toggleFavorite } = useFavorites()
const { user } = useAuth()
const [isAnimating, setIsAnimating] = useState(false)
const favorited = isFavorite(slug)
/** Handle click to toggle favorite status with animation */
const handleClick = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setIsAnimating(true)
// Always allow favorites (localStorage for anonymous users)
toggleFavorite(slug)
// Reset animation state
setTimeout(() => setIsAnimating(false), 200)
}
const sizeClasses = {
sm: 'h-8 w-8',
md: 'h-10 w-10',
lg: 'h-12 w-12'
}
const iconSizeClasses = {
sm: 'h-5 w-5',
md: 'h-6 w-6',
lg: 'h-7 w-7'
}
const baseClasses = cn(
'relative flex items-center justify-center rounded-full transition-all duration-200 z-20 cursor-pointer group/favorite',
'hover:scale-110 will-change-transform transform-gpu',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
sizeClasses[size],
{
'bg-background border shadow-sm': variant === 'default',
'hover:bg-muted/50': variant === 'ghost'
},
variant === 'default' &&
(favorited
? 'border-red-500 hover:border-red-600 dark:border-red-400 dark:hover:border-red-500'
: 'border-border hover:border-red-200 dark:hover:border-red-800'),
className
)
return (
<button
type="button"
onClick={handleClick}
className={baseClasses}
aria-label={favorited ? 'Remove from favorites' : 'Add to favorites'}
title={
favorited
? 'Remove from favorites'
: user
? 'Add to favorites'
: 'Add to favorites (save locally)'
}
>
<Heart
className={cn(
'transition-all duration-200 antialiased',
iconSizeClasses[size],
favorited
? 'fill-red-500 text-red-500 dark:fill-red-400 dark:text-red-400'
: 'text-muted-foreground group-hover/favorite:text-red-400 dark:group-hover/favorite:text-red-500',
isAnimating && 'scale-125'
)}
style={{
shapeRendering: 'auto',
vectorEffect: 'non-scaling-stroke'
}}
/>
</button>
)
}