Todos os componentes

Team Dashboard

Team Dashboard é um componente para React e Tailwind CSS, da biblioteca HextaUI, com licença MIT. Copia e cola no teu projeto.

HextaUI MIT other

O que é

Team Dashboard é um componente para React e Tailwind CSS, da biblioteca HextaUI, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Não precisa de pacotes além do que o projeto já tem

Licença

Este componente vem de HextaUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Team Dashboard

registry/new-york/blocks/team/team-dashboard.tsx
"use client";

import {
  Activity,
  Bot,
  FileText,
  MessageSquare,
  Plus,
  Settings,
  TrendingUp,
  Users,
} from "lucide-react";
import Link from "next/link";
import { cn } from "@/lib/utils";
import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/registry/new-york/ui/avatar";
import { Badge } from "@/registry/new-york/ui/badge";
import { Button } from "@/registry/new-york/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/registry/new-york/ui/card";
import { Progress } from "@/registry/new-york/ui/progress";
import { Separator } from "@/registry/new-york/ui/separator";

export interface TeamMember {
  id: string;
  name: string;
  avatar?: string;
  role: "owner" | "admin" | "member" | "viewer";
  status: "active" | "away" | "offline";
}

export interface TeamActivity {
  id: string;
  type: "member_joined" | "ai_session" | "file_uploaded" | "note_created";
  user: {
    name: string;
    avatar?: string;
  };
  description: string;
  timestamp: Date;
}

export interface TeamUsage {
  aiTokens: {
    used: number;
    limit: number;
  };
  storage: {
    used: number;
    limit: number;
  };
  members: {
    current: number;
    limit: number;
  };
}

export interface TeamDashboardProps {
  teamName: string;
  teamAvatar?: string;
  plan?: "free" | "pro" | "enterprise";
  members?: TeamMember[];
  recentActivities?: TeamActivity[];
  usage?: TeamUsage;
  onInviteMember?: () => void;
  onManageSettings?: () => void;
  className?: string;
}

function formatBytes(bytes: number): string {
  if (bytes === 0) return "0 Bytes";
  const k = 1024;
  const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`;
}

function formatNumber(num: number): string {
  if (num >= 1_000_000) {
    return `${(num / 1_000_000).toFixed(1)}M`;
  }
  if (num >= 1000) {
    return `${(num / 1000).toFixed(1)}K`;
  }
  return num.toString();
}

function formatTimeAgo(date: Date): string {
  const now = Date.now();
  const diff = now - date.getTime();
  const minutes = Math.floor(diff / 60_000);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);

  if (minutes < 1) return "Just now";
  if (minutes < 60) return `${minutes}m ago`;
  if (hours < 24) return `${hours}h ago`;
  if (days < 7) return `${days}d ago`;
  return new Intl.DateTimeFormat("en-US", {
    month: "short",
    day: "numeric",
  }).format(date);
}

function getActivityIcon(type: TeamActivity["type"]) {
  switch (type) {
    case "member_joined":
      return Users;
    case "ai_session":
      return Bot;
    case "file_uploaded":
      return FileText;
    case "note_created":
      return MessageSquare;
    default:
      return Activity;
  }
}

function getInitials(name: string): string {
  return name
    .split(" ")
    .map((n) => n[0])
    .join("")
    .toUpperCase()
    .slice(0, 2);
}

export default function TeamDashboard({
  teamName,
  teamAvatar,
  plan = "free",
  members = [],
  recentActivities = [],
  usage,
  onInviteMember,
  onManageSettings,
  className,
}: TeamDashboardProps) {
  const activeMembers = members.filter((m) => m.status === "active").length;
  const onlineMembers = members.filter((m) => m.status === "active").length;

  return (
    <div className={cn("flex w-full flex-col gap-6", className)}>
      {/* Header */}
      <div className="flex flex-col flex-wrap gap-4 md:flex-row md:items-center md:justify-between">
        <div className="flex items-center gap-4">
          <Avatar className="size-12">
            <AvatarImage alt={teamName} src={teamAvatar} />
            <AvatarFallback>{getInitials(teamName)}</AvatarFallback>
          </Avatar>
          <div className="flex flex-col gap-1">
            <h1 className="font-semibold text-xl">{teamName}</h1>
            <div className="flex items-center gap-2">
              <Badge variant="secondary">{plan}</Badge>
              <span className="text-muted-foreground text-sm">
                {members.length} member{members.length !== 1 ? "s" : ""}
              </span>
            </div>
          </div>
        </div>
        <div className="flex flex-wrap items-center gap-2">
          {onInviteMember && (
            <Button onClick={onInviteMember} type="button">
              <Plus className="size-4" />
              Invite Member
            </Button>
          )}
          {onManageSettings && (
            <Button onClick={onManageSettings} type="button" variant="outline">
              <Settings className="size-4" />
              Settings
            </Button>
          )}
        </div>
      </div>

      {/* Usage Stats */}
      {usage && (
        <div className="grid gap-4 md:grid-cols-3">
          <Card className="shadow-xs">
            <CardHeader className="pb-3">
              <CardTitle className="font-medium text-sm">AI Usage</CardTitle>
              <CardDescription className="text-muted-foreground">
                How many AI tokens your team used this month.
              </CardDescription>
            </CardHeader>
            <CardContent className="flex flex-col gap-2">
              <div className="flex items-baseline gap-2">
                <span className="font-semibold text-2xl">
                  {formatNumber(usage.aiTokens.used)}
                </span>
                <span className="text-muted-foreground text-sm">
                  / {formatNumber(usage.aiTokens.limit)} tokens
                </span>
              </div>
              <Progress
                value={(usage.aiTokens.used / usage.aiTokens.limit) * 100}
              />
            </CardContent>
          </Card>

          <Card className="shadow-xs">
            <CardHeader className="pb-3">
              <CardTitle className="font-medium text-sm">Storage</CardTitle>
              <CardDescription className="text-muted-foreground">
                Space your team is using for storing files.
              </CardDescription>
            </CardHeader>
            <CardContent className="flex flex-col gap-2">
              <div className="flex items-baseline gap-2">
                <span className="font-semibold text-2xl">
                  {formatBytes(usage.storage.used)}
                </span>
                <span className="text-muted-foreground text-sm">
                  / {formatBytes(usage.storage.limit)}
                </span>
              </div>
              <Progress
                value={(usage.storage.used / usage.storage.limit) * 100}
              />
            </CardContent>
          </Card>

          <Card className="shadow-xs">
            <CardHeader className="pb-3">
              <CardTitle className="font-medium text-sm">Members</CardTitle>
              <CardDescription className="text-muted-foreground">
                Number of people in your current team.
              </CardDescription>
            </CardHeader>
            <CardContent className="flex flex-col gap-2">
              <div className="flex items-baseline gap-2">
                <span className="font-semibold text-2xl">
                  {usage.members.current}
                </span>
                <span className="text-muted-foreground text-sm">
                  / {usage.members.limit} members
                </span>
              </div>
              <Progress
                value={(usage.members.current / usage.members.limit) * 100}
              />
            </CardContent>
          </Card>
        </div>
      )}

      <div className="grid gap-6 lg:grid-cols-2">
        {/* Quick Actions */}
        <Card className="shadow-xs">
          <CardHeader>
            <CardTitle>Quick Actions</CardTitle>
            <CardDescription>Common team operations</CardDescription>
          </CardHeader>
          <CardContent>
            <div className="grid gap-3 md:grid-cols-2">
              <Button
                asChild
                className="h-auto flex-col gap-2 py-4"
                variant="outline"
              >
                <Link href="#chat">
                  <MessageSquare className="size-5" />
                  <span>Team Chat</span>
                </Link>
              </Button>
              <Button
                asChild
                className="h-auto flex-col gap-2 py-4"
                variant="outline"
              >
                <Link href="#ai-room">
                  <Bot className="size-5" />
                  <span>AI Room</span>
                </Link>
              </Button>
              <Button
                asChild
                className="h-auto flex-col gap-2 py-4"
                variant="outline"
              >
                <Link href="#files">
                  <FileText className="size-5" />
                  <span>Files</span>
                </Link>
              </Button>
              <Button
                asChild
                className="h-auto flex-col gap-2 py-4"
                variant="outline"
              >
                <Link href="#analytics">
                  <TrendingUp className="size-5" />
                  <span>Analytics</span>
                </Link>
              </Button>
            </div>
          </CardContent>
        </Card>

        {/* Active Members */}
        <Card className="shadow-xs">
          <CardHeader>
            <CardTitle>Active Members</CardTitle>
            <CardDescription>
              {onlineMembers} member{onlineMembers !== 1 ? "s" : ""} online
            </CardDescription>
          </CardHeader>
          <CardContent>
            {members.length === 0 ? (
              <div className="flex flex-col items-center justify-center gap-2 py-8 text-center">
                <Users className="size-8 text-muted-foreground" />
                <p className="text-muted-foreground text-sm">No members yet</p>
              </div>
            ) : (
              <div className="flex flex-wrap gap-3">
                {members.slice(0, 8).map((member) => (
                  <div
                    className="flex flex-col items-center gap-2"
                    key={member.id}
                  >
                    <div className="relative">
                      <Avatar className="size-10">
                        <AvatarImage alt={member.name} src={member.avatar} />
                        <AvatarFallback>
                          {getInitials(member.name)}
                        </AvatarFallback>
                      </Avatar>
                      {member.status === "active" && (
                        <div className="absolute right-0 bottom-0 size-3 rounded-full border-2 border-background bg-green-500" />
                      )}
                    </div>
                    <span className="max-w-[60px] truncate text-xs">
                      {member.name}
                    </span>
                  </div>
                ))}
                {members.length > 8 && (
                  <div className="flex flex-col items-center gap-2">
                    <div className="flex size-10 items-center justify-center rounded-full border bg-muted">
                      <span className="text-muted-foreground text-xs">
                        +{members.length - 8}
                      </span>
                    </div>
                  </div>
                )}
              </div>
            )}
          </CardContent>
        </Card>
      </div>

      {/* Recent Activity */}
      {recentActivities.length > 0 && (
        <Card className="shadow-xs">
          <CardHeader>
            <CardTitle>Recent Activity</CardTitle>
            <CardDescription>Latest team updates</CardDescription>
          </CardHeader>
          <CardContent>
            <div className="flex flex-col gap-4">
              {recentActivities.slice(0, 5).map((activity, idx) => {
                const Icon = getActivityIcon(activity.type);
                return (
                  <div key={activity.id}>
                    <div className="flex items-start gap-3">
                      <Avatar className="size-8 shrink-0">
                        <AvatarImage
                          alt={activity.user.name}
                          src={activity.user.avatar}
                        />
                        <AvatarFallback className="text-xs">
                          {getInitials(activity.user.name)}
                        </AvatarFallback>
                      </Avatar>
                      <div className="flex min-w-0 flex-1 flex-col gap-2">
                        <div className="flex items-center gap-2">
                          <span className="font-medium text-sm">
                            {activity.user.name}
                          </span>
                          <div className="flex size-4 items-center justify-center rounded-full text-muted-foreground">
                            <Icon className="size-3" />
                          </div>
                        </div>
                        <p className="text-muted-foreground text-sm">
                          {activity.description}
                        </p>
                        <span className="text-muted-foreground text-xs">
                          {formatTimeAgo(activity.timestamp)}
                        </span>
                      </div>
                    </div>
                    {idx < recentActivities.slice(0, 5).length - 1 && (
                      <Separator className="mt-4" />
                    )}
                  </div>
                );
              })}
            </div>
          </CardContent>
        </Card>
      )}
    </div>
  );
}

Componentes parecidos