mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-07 08:26:00 +00:00
feat(search): allow unstar from skill cards
This commit is contained in:
parent
bbec999d49
commit
2f1faa1397
3 changed files with 99 additions and 62 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { SkillSummary } from '@/api/types'
|
||||
import { useAuth } from '@/features/auth/use-auth'
|
||||
import { useStar } from '@/features/social/use-star'
|
||||
import { useStar, useToggleStar } from '@/features/social/use-star'
|
||||
import { ConfirmDialog } from '@/shared/components/confirm-dialog'
|
||||
import { Card } from '@/shared/ui/card'
|
||||
import { NamespaceBadge } from '@/shared/components/namespace-badge'
|
||||
import { Bookmark } from 'lucide-react'
|
||||
|
|
@ -15,74 +17,101 @@ interface SkillCardProps {
|
|||
export function SkillCard({ skill, onClick, highlightStarred = true }: SkillCardProps) {
|
||||
const { t } = useTranslation()
|
||||
const { isAuthenticated } = useAuth()
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
const { data: starStatus } = useStar(skill.id, highlightStarred && isAuthenticated)
|
||||
const toggleStarMutation = useToggleStar(skill.id)
|
||||
const showStarredBadge = highlightStarred && isAuthenticated && starStatus?.starred
|
||||
|
||||
const handleStarredBadgeClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation()
|
||||
setConfirmOpen(true)
|
||||
}
|
||||
|
||||
const handleConfirmUnstar = async () => {
|
||||
if (!starStatus?.starred) {
|
||||
return
|
||||
}
|
||||
await toggleStarMutation.mutateAsync(starStatus.starred)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="h-full p-5 cursor-pointer group relative overflow-hidden"
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Hover gradient border effect */}
|
||||
<div className="absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gradient-to-br from-primary/20 via-transparent to-accent/20 pointer-events-none" />
|
||||
<>
|
||||
<Card
|
||||
className="h-full p-5 cursor-pointer group relative overflow-hidden"
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Hover gradient border effect */}
|
||||
<div className="absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-gradient-to-br from-primary/20 via-transparent to-accent/20 pointer-events-none" />
|
||||
|
||||
<div className="relative z-10 flex h-full flex-col">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold font-heading text-lg text-foreground group-hover:text-primary transition-colors">
|
||||
{skill.displayName}
|
||||
</h3>
|
||||
<div className="relative z-10 flex h-full flex-col">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-semibold font-heading text-lg text-foreground group-hover:text-primary transition-colors">
|
||||
{skill.displayName}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{showStarredBadge ? (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 rounded-full border border-primary/25 bg-primary/12 px-2.5 py-1 text-[11px] font-semibold text-primary shadow-sm transition-colors hover:bg-primary/18"
|
||||
aria-label={t('skillCard.starred')}
|
||||
title={t('skillCard.starredAction')}
|
||||
onClick={handleStarredBadgeClick}
|
||||
>
|
||||
<Bookmark className="h-3.5 w-3.5 fill-current" />
|
||||
{t('skillCard.starred')}
|
||||
</button>
|
||||
) : null}
|
||||
<NamespaceBadge type="TEAM" name={`@${skill.namespace}`} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{showStarredBadge ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 rounded-full border border-primary/25 bg-primary/12 px-2.5 py-1 text-[11px] font-semibold text-primary shadow-sm"
|
||||
aria-label={t('skillCard.starred')}
|
||||
title={t('skillCard.starred')}
|
||||
>
|
||||
<Bookmark className="h-3.5 w-3.5 fill-current" />
|
||||
{t('skillCard.starred')}
|
||||
|
||||
{skill.summary && (
|
||||
<p className="text-sm text-muted-foreground mb-4 line-clamp-2 leading-relaxed">
|
||||
{skill.summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-auto flex items-center gap-4 text-xs text-muted-foreground">
|
||||
{skill.latestVersion && (
|
||||
<span className="px-2.5 py-1 rounded-full bg-secondary/60 font-mono">
|
||||
v{skill.latestVersion}
|
||||
</span>
|
||||
) : null}
|
||||
<NamespaceBadge type="TEAM" name={`@${skill.namespace}`} />
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
|
||||
</svg>
|
||||
{skill.downloadCount}
|
||||
</span>
|
||||
<span
|
||||
className={`flex items-center gap-1 ${showStarredBadge ? 'font-semibold text-primary' : ''}`}
|
||||
aria-label={showStarredBadge ? t('skillCard.starred') : undefined}
|
||||
>
|
||||
<Bookmark className={`w-3.5 h-3.5 ${showStarredBadge ? 'fill-current' : ''}`} />
|
||||
{skill.starCount}
|
||||
</span>
|
||||
{skill.ratingAvg !== undefined && skill.ratingCount > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<svg className="w-3.5 h-3.5 text-primary" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
{skill.ratingAvg.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{skill.summary && (
|
||||
<p className="text-sm text-muted-foreground mb-4 line-clamp-2 leading-relaxed">
|
||||
{skill.summary}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-auto flex items-center gap-4 text-xs text-muted-foreground">
|
||||
{skill.latestVersion && (
|
||||
<span className="px-2.5 py-1 rounded-full bg-secondary/60 font-mono">
|
||||
v{skill.latestVersion}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10" />
|
||||
</svg>
|
||||
{skill.downloadCount}
|
||||
</span>
|
||||
<span
|
||||
className={`flex items-center gap-1 ${showStarredBadge ? 'font-semibold text-primary' : ''}`}
|
||||
aria-label={showStarredBadge ? t('skillCard.starred') : undefined}
|
||||
>
|
||||
<Bookmark className={`w-3.5 h-3.5 ${showStarredBadge ? 'fill-current' : ''}`} />
|
||||
{skill.starCount}
|
||||
</span>
|
||||
{skill.ratingAvg !== undefined && skill.ratingCount > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<svg className="w-3.5 h-3.5 text-primary" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
{skill.ratingAvg.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<ConfirmDialog
|
||||
open={confirmOpen}
|
||||
onOpenChange={setConfirmOpen}
|
||||
title={t('skillCard.unstarTitle')}
|
||||
description={t('skillCard.unstarDescription', { name: skill.displayName })}
|
||||
confirmText={t('skillCard.unstarConfirm')}
|
||||
onConfirm={handleConfirmUnstar}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -416,7 +416,11 @@
|
|||
"star": "Star"
|
||||
},
|
||||
"skillCard": {
|
||||
"starred": "Starred"
|
||||
"starred": "Starred",
|
||||
"starredAction": "Click to unstar",
|
||||
"unstarTitle": "Remove from starred",
|
||||
"unstarDescription": "Are you sure you want to remove \"{{name}}\" from your starred skills?",
|
||||
"unstarConfirm": "Remove"
|
||||
},
|
||||
"copyButton": {
|
||||
"copied": "Copied",
|
||||
|
|
|
|||
|
|
@ -416,7 +416,11 @@
|
|||
"star": "收藏"
|
||||
},
|
||||
"skillCard": {
|
||||
"starred": "已收藏"
|
||||
"starred": "已收藏",
|
||||
"starredAction": "点击取消收藏",
|
||||
"unstarTitle": "取消收藏",
|
||||
"unstarDescription": "确定要取消收藏技能“{{name}}”吗?",
|
||||
"unstarConfirm": "取消收藏"
|
||||
},
|
||||
"copyButton": {
|
||||
"copied": "已复制",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue