Files
gooseek/services/web-svc/src/components/Widgets/UnifiedCard.tsx
home 06fe57c765 feat: Go backend, enhanced search, new widgets, Docker deploy
Major changes:
- Add Go backend (backend/) with microservices architecture
- Enhanced master-agents-svc: reranker, content-classifier, stealth-crawler,
  proxy-manager, media-search, fastClassifier, language detection
- New web-svc widgets: KnowledgeCard, ProductCard, ProfileCard, VideoCard,
  UnifiedCard, CardGallery, InlineImageGallery, SourcesPanel, RelatedQuestions
- Improved discover-svc with discover-db integration
- Docker deployment improvements (Caddyfile, vendor.sh, BUILD.md)
- Library-svc: project_id schema migration
- Remove deprecated finance-svc and travel-svc
- Localization improvements across services

Made-with: Cursor
2026-02-27 04:15:32 +03:00

230 lines
6.8 KiB
TypeScript

'use client';
import { ReactNode } from 'react';
import { ExternalLink } from 'lucide-react';
import { cn } from '@/lib/utils';
export interface UnifiedCardProps {
type: 'product' | 'video' | 'profile' | 'promo' | 'article';
image?: string;
imageAlt?: string;
title: string;
subtitle?: string;
badge?: {
text: string;
variant: 'success' | 'warning' | 'error' | 'info' | 'default';
};
meta?: string[];
action?: {
label: string;
url: string;
variant?: 'primary' | 'secondary';
};
secondaryAction?: {
label: string;
onClick: () => void;
};
children?: ReactNode;
className?: string;
href?: string;
compact?: boolean;
horizontal?: boolean;
}
const badgeVariants = {
success: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
warning: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
error: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
info: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
default: 'bg-light-200 text-black/70 dark:bg-dark-200 dark:text-white/70',
};
const PLACEHOLDER_IMAGE =
'data:image/svg+xml,' +
encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"><rect fill="%23f3f4f6" width="200" height="200"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="%239ca3af" font-family="sans-serif" font-size="14">No Image</text></svg>'
);
export function UnifiedCard({
type,
image,
imageAlt,
title,
subtitle,
badge,
meta,
action,
secondaryAction,
children,
className,
href,
compact = false,
horizontal = false,
}: UnifiedCardProps) {
const CardWrapper = href ? 'a' : 'div';
const wrapperProps = href
? { href, target: '_blank', rel: 'noopener noreferrer' }
: {};
return (
<CardWrapper
{...wrapperProps}
className={cn(
'group relative flex overflow-hidden',
'bg-white dark:bg-dark-secondary',
'border border-light-200 dark:border-dark-200',
'rounded-xl shadow-sm',
'transition-all duration-200',
href && 'cursor-pointer hover:shadow-md hover:border-[#EA580C]/30',
horizontal ? 'flex-row' : 'flex-col',
compact ? 'p-2' : 'p-0',
className
)}
>
{image && (
<div
className={cn(
'relative overflow-hidden bg-light-100 dark:bg-dark-100',
horizontal
? compact
? 'w-16 h-16 rounded-lg flex-shrink-0'
: 'w-24 h-24 rounded-l-xl flex-shrink-0'
: compact
? 'w-full h-24'
: 'w-full aspect-video'
)}
>
<img
src={image}
alt={imageAlt || title}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
loading="lazy"
onError={(e) => {
(e.target as HTMLImageElement).src = PLACEHOLDER_IMAGE;
}}
/>
{badge && (
<span
className={cn(
'absolute top-2 left-2 px-2 py-0.5 rounded-full text-[10px] font-semibold',
badgeVariants[badge.variant]
)}
>
{badge.text}
</span>
)}
</div>
)}
<div
className={cn(
'flex flex-col flex-1 min-w-0',
horizontal ? (compact ? 'pl-2' : 'p-3') : compact ? 'pt-2' : 'p-3'
)}
>
<div className="flex-1 min-w-0">
{!image && badge && (
<span
className={cn(
'inline-block mb-1.5 px-2 py-0.5 rounded-full text-[10px] font-semibold',
badgeVariants[badge.variant]
)}
>
{badge.text}
</span>
)}
<h4
className={cn(
'font-semibold text-black dark:text-white leading-tight',
compact ? 'text-xs line-clamp-2' : 'text-sm line-clamp-2'
)}
>
{title}
</h4>
{subtitle && (
<p
className={cn(
'text-black/60 dark:text-white/60 mt-0.5',
compact ? 'text-[10px] line-clamp-1' : 'text-xs line-clamp-2'
)}
>
{subtitle}
</p>
)}
{meta && meta.length > 0 && (
<div
className={cn(
'flex flex-wrap items-center gap-x-2 gap-y-0.5 mt-1.5',
compact ? 'text-[9px]' : 'text-[10px]'
)}
>
{meta.map((item, i) => (
<span
key={i}
className="text-black/50 dark:text-white/50 whitespace-nowrap"
>
{item}
</span>
))}
</div>
)}
{children}
</div>
{(action || secondaryAction) && !compact && (
<div className="flex items-center gap-2 mt-3 pt-2 border-t border-light-200/50 dark:border-dark-200/50">
{action && (
<a
href={action.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className={cn(
'flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors',
action.variant === 'secondary'
? 'bg-light-100 dark:bg-dark-100 text-black/70 dark:text-white/70 hover:bg-light-200 dark:hover:bg-dark-200'
: 'bg-[#EA580C] text-white hover:bg-[#EA580C]/90'
)}
>
{action.label}
<ExternalLink size={10} />
</a>
)}
{secondaryAction && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
secondaryAction.onClick();
}}
className="px-3 py-1.5 rounded-lg text-xs font-medium
bg-light-100 dark:bg-dark-100
text-black/70 dark:text-white/70
hover:bg-light-200 dark:hover:bg-dark-200
transition-colors"
>
{secondaryAction.label}
</button>
)}
</div>
)}
</div>
{href && (
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
<ExternalLink
size={14}
className="text-black/30 dark:text-white/30"
/>
</div>
)}
</CardWrapper>
);
}
export default UnifiedCard;