Ctrl K
ComunidadGuías…

src/components/notifications.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"use client";

import Link from "next/link";
import {
  BellIcon,
  CalendarClockIcon,
  CircleUserRoundIcon,
  HandshakeIcon,
  ListChecksIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";

/**
 * Everything that has come round, in the chrome.
 *
 * This used to be a card on the dashboard called "Needs you now", which meant
 * you only saw what was owed if you happened to be on that page. A bell in the
 * top bar is on every page, which is the whole point of a thing that is trying
 * to interrupt you.
 *
 * One count, four groups. The count is a single number because two numbers in
 * the chrome make you do arithmetic; the groups stay apart inside because
 * chasing a company, pinging a person, ticking a task and answering an offer
 * are four different actions and a flat list of look-alike rows hides which is
 * which.
 *
 * An offer deadline sits at the top of the list. It is the only date here that
 * cannot be caught up on tomorrow.
 *
 * Nothing here is dismissable, and that is deliberate: an item leaves this list
 * by being dealt with — logging the follow-up, moving the ping, ticking the
 * task — not by being swiped away. A dismissable notification would let the
 * bell go quiet while the work stayed undone.
 */
export type Notice = {
  kind: "APPLICATION" | "CONTACT" | "TASK" | "OFFER";
  id: string;
  title: string;
  detail: string;
  due: string;
  overdue: boolean;
};

const GROUPS = [
  {
    key: "OFFER" as const,
    label: "Answer by",
    icon: HandshakeIcon,
    href: (id: string) => `/applications/${id}`,
  },
  {
    key: "APPLICATION" as const,
    label: "Follow up",
    icon: CalendarClockIcon,
    href: (id: string) => `/applications/${id}`,
  },
  {
    key: "CONTACT" as const,
    label: "Ping",
    icon: CircleUserRoundIcon,
    href: (id: string) => `/crm/contacts/${id}`,
  },
  { key: "TASK" as const, label: "Due", icon: ListChecksIcon, href: () => "/" },
];

export function Notifications({ items }: { items: Notice[] }) {
  const overdue = items.filter((item) => item.overdue).length;

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button
          variant="ghost"
          size="icon-sm"
          className="relative size-9"
          aria-label={
            items.length === 0
              ? "Nothing due"
              : `${items.length} thing${items.length === 1 ? "" : "s"} due`
          }
        >
          <BellIcon />
          {items.length > 0 && (
            // The number, not just a dot: "3" and "18" are different days.
            <span
              className={cn(
                "absolute -top-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-semibold tabular-nums",
                overdue > 0
                  ? "bg-destructive text-white"
                  : "bg-primary text-primary-foreground",
              )}
            >
              {items.length > 99 ? "99+" : items.length}
            </span>
          )}
        </Button>
      </PopoverTrigger>
      <PopoverContent align="end" className="w-80 p-0">
        <div className="flex items-baseline justify-between border-b px-3.5 py-2.5">
          <span className="text-[13px] font-medium">Needs you now</span>
          {overdue > 0 && (
            <span className="text-destructive text-[11.5px] font-medium tabular-nums">
              {overdue} overdue
            </span>
          )}
        </div>

        {items.length === 0 ? (
          <p className="text-muted-foreground px-3.5 py-8 text-center text-[13px]">
            Nothing has come round. Dates you set on applications, people and tasks show up here
            when they arrive.
          </p>
        ) : (
          <div className="max-h-[26rem] overflow-y-auto py-1">
            {GROUPS.map((group) => {
              const rows = items.filter((item) => item.kind === group.key);
              if (rows.length === 0) return null;
              return (
                <div key={group.key} className="py-1">
                  <div className="text-faint meta flex items-center gap-1.5 px-3.5 pt-1 pb-1.5 text-[10.5px] font-medium uppercase">
                    <group.icon className="size-3" />
                    {group.label}
                    <span className="tabular-nums">{rows.length}</span>
                  </div>
                  {rows.map((row) => (
                    <Link
                      key={`${row.kind}-${row.id}`}
                      href={group.href(row.id)}
                      className="hover:bg-accent flex items-baseline gap-2 px-3.5 py-1.5 transition-colors"
                    >
                      <span className="min-w-0 flex-1">
                        <span className="block truncate text-[13px] font-medium">{row.title}</span>
                        {row.detail && (
                          <span className="text-muted-foreground block truncate text-[11.5px]">
                            {row.detail}
                          </span>
                        )}
                      </span>
                      <span
                        className={cn(
                          "meta shrink-0 text-[11px] tabular-nums",
                          row.overdue ? "text-destructive" : "text-faint",
                        )}
                      >
                        {row.due}
                      </span>
                    </Link>
                  ))}
                </div>
              );
            })}
          </div>
        )}
      </PopoverContent>
    </Popover>
  );
}