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
230 lines
6.8 KiB
TypeScript
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;
|