card: single VIDraft name with stacked avatar group; drop JGOS-Model and Powergen-AI
c60c951 verified | import React from "react"; | |
| import { ProviderInfo } from "../types/heatmap"; | |
| import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"; | |
| interface OrganizationHeaderProps { | |
| provider: ProviderInfo; | |
| calendarKey: string; | |
| providerName: string; | |
| totalCount: number; | |
| } | |
| const OrganizationHeader: React.FC<OrganizationHeaderProps> = ({ | |
| provider, | |
| calendarKey, | |
| providerName, | |
| totalCount, | |
| }) => { | |
| return ( | |
| <TooltipProvider> | |
| <div className="mb-4"> | |
| {/* Organization Name & Stats Badge */} | |
| <div className="text-center bg-muted/20 rounded-lg p-3 border border-border/30"> | |
| {/* Avatar and Name Row */} | |
| <div className="flex items-center justify-center gap-2 mb-2"> | |
| {/* Multi-author avatars and names */} | |
| {provider.authorsData && provider.authorsData.length > 1 ? ( | |
| <div className="flex items-center gap-2 justify-center"> | |
| {/* 아이콘만 겹쳐서 그룹화 — 이름은 대표(첫 번째)만 표시 */} | |
| <div className="flex items-center" title={provider.authorsData.map(a => a.author).join(" · ")}> | |
| {provider.authorsData.slice(0, 6).map((authorData, index) => ( | |
| <img | |
| key={authorData.author} | |
| src={authorData.avatarUrl || `https://huggingface.co/${authorData.author}/avatar.jpg`} | |
| alt={`${authorData.fullName} logo`} | |
| title={authorData.author} | |
| className="w-7 h-7 rounded-md shadow-sm border-2 border-background bg-background" | |
| style={{ marginLeft: index === 0 ? 0 : -8, zIndex: 10 - index }} | |
| onError={(e) => { | |
| const target = e.target as HTMLImageElement; | |
| target.src = `https://ui-avatars.com/api/?name=${encodeURIComponent(authorData.fullName)}&background=random`; | |
| }} | |
| /> | |
| ))} | |
| {provider.authorsData.length > 6 && ( | |
| <span | |
| className="ml-1 text-[11px] font-bold text-muted-foreground bg-muted rounded px-1.5 py-0.5 border border-border/50" | |
| title={provider.authorsData.slice(6).map(a => a.author).join(" · ")} | |
| > | |
| +{provider.authorsData.length - 6} | |
| </span> | |
| )} | |
| </div> | |
| <h3 className="text-base font-bold text-foreground"> | |
| <a | |
| href={`https://huggingface.co/${provider.authorsData[0].author}`} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="hover:text-blue-500 hover:underline transition-colors duration-200" | |
| > | |
| {provider.authorsData[0].author} | |
| </a> | |
| </h3> | |
| </div> | |
| ) : ( | |
| <div className="flex items-center gap-2"> | |
| {provider.avatarUrl && ( | |
| <div className="relative"> | |
| <img | |
| src={provider.avatarUrl} | |
| alt={`${providerName} logo`} | |
| className="w-8 h-8 rounded-md shadow-sm border border-border/50" | |
| /> | |
| {provider.isVerified && ( | |
| <div className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-blue-500 rounded-full flex items-center justify-center"> | |
| <svg className="w-2 h-2 text-white" fill="currentColor" viewBox="0 0 20 20"> | |
| <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" /> | |
| </svg> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| <h3 className="text-base font-bold text-foreground"> | |
| <a | |
| href={`https://huggingface.co/${calendarKey}`} | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="hover:text-blue-500 hover:underline transition-colors duration-200" | |
| > | |
| {providerName} | |
| </a> | |
| </h3> | |
| </div> | |
| )} | |
| </div> | |
| {/* Compact Organization Stats */} | |
| <div className="flex flex-wrap justify-center gap-2 text-xs mb-3"> | |
| {provider.authorsData && provider.authorsData.length > 1 ? ( | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <div className="bg-background/60 rounded px-2 py-1 border border-border/40 cursor-pointer"> | |
| <span className="font-semibold text-foreground">{(provider.numModels || 0).toLocaleString()}</span> | |
| <span className="text-muted-foreground ml-1">models</span> | |
| </div> | |
| </TooltipTrigger> | |
| <TooltipContent side="bottom"> | |
| <div className="text-xs"> | |
| {provider.authorsData.map(author => ( | |
| <div key={`models-${author.author}`}> | |
| {author.author}: {author.numModels.toLocaleString()} | |
| </div> | |
| ))} | |
| </div> | |
| </TooltipContent> | |
| </Tooltip> | |
| ) : ( | |
| <div className="bg-background/60 rounded px-2 py-1 border border-border/40"> | |
| <span className="font-semibold text-foreground">{(provider.numModels || 0).toLocaleString()}</span> | |
| <span className="text-muted-foreground ml-1">models</span> | |
| </div> | |
| )} | |
| {provider.authorsData && provider.authorsData.length > 1 ? ( | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <div className="bg-background/60 rounded px-2 py-1 border border-border/40 cursor-pointer"> | |
| <span className="font-semibold text-foreground">{(provider.numDatasets || 0).toLocaleString()}</span> | |
| <span className="text-muted-foreground ml-1">datasets</span> | |
| </div> | |
| </TooltipTrigger> | |
| <TooltipContent side="bottom"> | |
| <div className="text-xs"> | |
| {provider.authorsData.map(author => ( | |
| <div key={`datasets-${author.author}`}> | |
| {author.author}: {author.numDatasets.toLocaleString()} | |
| </div> | |
| ))} | |
| </div> | |
| </TooltipContent> | |
| </Tooltip> | |
| ) : ( | |
| <div className="bg-background/60 rounded px-2 py-1 border border-border/40"> | |
| <span className="font-semibold text-foreground">{(provider.numDatasets || 0).toLocaleString()}</span> | |
| <span className="text-muted-foreground ml-1">datasets</span> | |
| </div> | |
| )} | |
| {provider.authorsData && provider.authorsData.length > 1 ? ( | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <div className="bg-background/60 rounded px-2 py-1 border border-border/40 cursor-pointer"> | |
| <span className="font-semibold text-foreground">{(provider.numSpaces || 0).toLocaleString()}</span> | |
| <span className="text-muted-foreground ml-1">spaces</span> | |
| </div> | |
| </TooltipTrigger> | |
| <TooltipContent side="bottom"> | |
| <div className="text-xs"> | |
| {provider.authorsData.map(author => ( | |
| <div key={`spaces-${author.author}`}> | |
| {author.author}: {author.numSpaces.toLocaleString()} | |
| </div> | |
| ))} | |
| </div> | |
| </TooltipContent> | |
| </Tooltip> | |
| ) : ( | |
| <div className="bg-background/60 rounded px-2 py-1 border border-border/40"> | |
| <span className="font-semibold text-foreground">{(provider.numSpaces || 0).toLocaleString()}</span> | |
| <span className="text-muted-foreground ml-1">spaces</span> | |
| </div> | |
| )} | |
| {provider.authorsData && provider.authorsData.length > 1 ? ( | |
| <Tooltip> | |
| <TooltipTrigger asChild> | |
| <div className="bg-background/60 rounded px-2 py-1 border border-border/40 cursor-pointer"> | |
| <span className="font-semibold text-foreground">{(provider.numFollowers || 0).toLocaleString()}</span> | |
| <span className="text-muted-foreground ml-1">followers</span> | |
| </div> | |
| </TooltipTrigger> | |
| <TooltipContent side="bottom"> | |
| <div className="text-xs"> | |
| {provider.authorsData.map(author => ( | |
| <div key={`followers-${author.author}`}> | |
| {author.author}: {author.numFollowers.toLocaleString()} | |
| </div> | |
| ))} | |
| </div> | |
| </TooltipContent> | |
| </Tooltip> | |
| ) : ( | |
| <div className="bg-background/60 rounded px-2 py-1 border border-border/40"> | |
| <span className="font-semibold text-foreground">{(provider.numFollowers || 0).toLocaleString()}</span> | |
| <span className="text-muted-foreground ml-1">followers</span> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </TooltipProvider> | |
| ); | |
| }; | |
| export default OrganizationHeader; |