Web InnoventixFreeCode

Profile User Card

Original · free

compact user profile card

byWeb InnoventixReact + Tailwind
profileusercardprofiles
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/profile-user-card.json
profile-user-card.tsx
"use client";

import { useCallback, useId, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type TabId = "activity" | "stack" | "hours";

type Tab = {
  id: TabId;
  label: string;
};

type ActivityRow = {
  repo: string;
  action: string;
  when: string;
  additions: number;
  deletions: number;
};

type StackItem = {
  name: string;
  level: number;
  note: string;
};

const TABS: Tab[] = [
  { id: "activity", label: "Activity" },
  { id: "stack", label: "Stack" },
  { id: "hours", label: "Hours" },
];

const ACTIVITY: ActivityRow[] = [
  {
    repo: "orbital/edge-router",
    action: "merged pull request #412 — cache warm-up on cold start",
    when: "2h ago",
    additions: 184,
    deletions: 96,
  },
  {
    repo: "orbital/design-tokens",
    action: "published v3.1.0 — contrast fixes for dark surfaces",
    when: "Yesterday",
    additions: 61,
    deletions: 12,
  },
  {
    repo: "kettle/observability",
    action: "opened issue #77 — trace sampling drops on retry",
    when: "3 days ago",
    additions: 0,
    deletions: 0,
  },
];

const STACK: StackItem[] = [
  { name: "TypeScript", level: 92, note: "6 yrs · strict everywhere" },
  { name: "Rust", level: 68, note: "2 yrs · router internals" },
  { name: "PostgreSQL", level: 81, note: "5 yrs · query tuning" },
  { name: "Terraform", level: 54, note: "1 yr · staging only" },
];

const HOURS: number[] = [1, 0, 2, 5, 8, 6, 3, 4, 7, 9, 6, 2];
const HOUR_LABELS = ["8a", "9a", "10a", "11a", "12p", "1p", "2p", "3p", "4p", "5p", "6p", "7p"];

export default function ProfileUserCard() {
  const reduce = useReducedMotion();
  const [tab, setTab] = useState<TabId>("activity");
  const [following, setFollowing] = useState(false);
  const [copied, setCopied] = useState(false);
  const tabRefs = useRef<Record<TabId, HTMLButtonElement | null>>({
    activity: null,
    stack: null,
    hours: null,
  });
  const copyTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
  const baseId = useId();

  const followers = useMemo(() => (following ? 2431 : 2430), [following]);

  const onTabKeyDown = useCallback(
    (event: React.KeyboardEvent<HTMLButtonElement>) => {
      const index = TABS.findIndex((t) => t.id === tab);
      let next = -1;
      if (event.key === "ArrowRight") next = (index + 1) % TABS.length;
      else if (event.key === "ArrowLeft") next = (index - 1 + TABS.length) % TABS.length;
      else if (event.key === "Home") next = 0;
      else if (event.key === "End") next = TABS.length - 1;
      if (next < 0) return;
      event.preventDefault();
      const nextId = TABS[next].id;
      setTab(nextId);
      tabRefs.current[nextId]?.focus();
    },
    [tab],
  );

  const copyHandle = useCallback(() => {
    const write = navigator.clipboard?.writeText("@nadia.brar");
    if (copyTimer.current) clearTimeout(copyTimer.current);
    const done = () => {
      setCopied(true);
      copyTimer.current = setTimeout(() => setCopied(false), 1800);
    };
    if (write) write.then(done).catch(done);
    else done();
  }, []);

  const peak = Math.max(...HOURS);

  return (
    <section className="relative w-full bg-slate-50 px-4 py-20 sm:px-6 sm:py-28 dark:bg-slate-950">
      <style>{`
        @keyframes puc-pulse-ring {
          0% { transform: scale(0.85); opacity: 0.75; }
          70% { transform: scale(1.9); opacity: 0; }
          100% { transform: scale(1.9); opacity: 0; }
        }
        @keyframes puc-sheen {
          0% { transform: translateX(-120%); }
          100% { transform: translateX(220%); }
        }
        .puc-ring::after {
          content: "";
          position: absolute;
          inset: 0;
          border-radius: 9999px;
          background-color: rgb(16 185 129 / 0.55);
          animation: puc-pulse-ring 2.6s ease-out infinite;
        }
        .puc-sheen::before {
          content: "";
          position: absolute;
          top: 0;
          bottom: 0;
          width: 40%;
          background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.18), transparent);
          animation: puc-sheen 3.4s ease-in-out infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .puc-ring::after,
          .puc-sheen::before {
            animation: none;
          }
          .puc-sheen::before { display: none; }
        }
      `}</style>

      <div className="mx-auto w-full max-w-sm">
        <motion.article
          initial={reduce ? false : { opacity: 0, y: 14 }}
          whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
          viewport={{ once: true, amount: 0.4 }}
          transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
          aria-labelledby={`${baseId}-name`}
          className="overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-[0_1px_2px_rgb(15_23_42_/_0.04),0_12px_36px_-12px_rgb(15_23_42_/_0.14)] dark:border-slate-800 dark:bg-slate-900 dark:shadow-[0_12px_40px_-16px_rgb(0_0_0_/_0.8)]"
        >
          <div className="puc-sheen relative h-24 overflow-hidden bg-gradient-to-br from-indigo-500 via-violet-500 to-sky-400">
            <svg
              aria-hidden="true"
              className="absolute inset-0 h-full w-full opacity-30"
              viewBox="0 0 400 96"
              preserveAspectRatio="none"
            >
              <path
                d="M0 72 C 60 40, 100 88, 160 60 S 280 24, 340 56 S 400 80, 400 80 L400 96 L0 96 Z"
                fill="rgb(255 255 255 / 0.25)"
              />
              <path
                d="M0 84 C 70 60, 120 96, 190 74 S 300 48, 400 72 L400 96 L0 96 Z"
                fill="rgb(255 255 255 / 0.2)"
              />
            </svg>
          </div>

          <div className="px-6 pb-6">
            <div className="-mt-10 flex items-end justify-between gap-3">
              <div className="relative">
                <div
                  aria-hidden="true"
                  className="grid h-20 w-20 place-items-center rounded-full border-4 border-white bg-gradient-to-br from-violet-600 to-indigo-600 text-2xl font-semibold tracking-tight text-white select-none dark:border-slate-900"
                >
                  NB
                </div>
                <span className="absolute right-0.5 bottom-0.5 grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-white dark:border-slate-900 dark:bg-slate-900">
                  <span className="puc-ring relative block h-2.5 w-2.5 rounded-full bg-emerald-500" />
                  <span className="sr-only">Online now</span>
                </span>
              </div>

              <div className="flex items-center gap-2 pb-1">
                <button
                  type="button"
                  onClick={copyHandle}
                  className="inline-flex h-9 items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 text-xs font-medium text-slate-600 transition-colors hover:bg-slate-50 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
                >
                  <svg
                    aria-hidden="true"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="1.75"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    className="h-3.5 w-3.5"
                  >
                    {copied ? (
                      <path d="M20 6 9 17l-5-5" />
                    ) : (
                      <>
                        <rect x="9" y="9" width="11" height="11" rx="2" />
                        <path d="M5 15V5a2 2 0 0 1 2-2h10" />
                      </>
                    )}
                  </svg>
                  {copied ? "Copied" : "Handle"}
                </button>

                <button
                  type="button"
                  aria-pressed={following}
                  onClick={() => setFollowing((v) => !v)}
                  className={[
                    "inline-flex h-9 items-center rounded-full px-4 text-xs font-semibold transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:focus-visible:ring-offset-slate-900",
                    following
                      ? "border border-slate-200 bg-slate-100 text-slate-700 hover:bg-slate-200 focus-visible:ring-slate-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700"
                      : "bg-indigo-600 text-white hover:bg-indigo-500 focus-visible:ring-indigo-500",
                  ].join(" ")}
                >
                  {following ? "Following" : "Follow"}
                </button>
              </div>
            </div>

            <div className="mt-4">
              <div className="flex items-center gap-1.5">
                <h2
                  id={`${baseId}-name`}
                  className="text-lg font-semibold tracking-tight text-slate-900 dark:text-slate-50"
                >
                  Nadia Brar
                </h2>
                <svg
                  viewBox="0 0 24 24"
                  aria-hidden="true"
                  className="h-4 w-4 shrink-0 text-sky-500"
                  fill="currentColor"
                >
                  <path d="M12 1.6l2.3 2.06 3.06-.35 1.06 2.9 2.9 1.06-.35 3.06L23 12l-2.03 2.67.35 3.06-2.9 1.06-1.06 2.9-3.06-.35L12 22.4l-2.3-2.06-3.06.35-1.06-2.9-2.9-1.06.35-3.06L1 12l2.03-2.67-.35-3.06 2.9-1.06 1.06-2.9 3.06.35L12 1.6zm-1.2 13.4l5.3-5.3-1.42-1.42-3.88 3.88-1.76-1.76L7.6 11.8l3.2 3.2z" />
                </svg>
                <span className="sr-only">Verified maintainer</span>
              </div>
              <p className="mt-0.5 text-sm text-slate-500 dark:text-slate-400">
                Staff engineer, Edge Platform · @nadia.brar
              </p>
              <p className="mt-3 text-sm leading-relaxed text-slate-600 dark:text-slate-300">
                I keep request latency boring. Currently rewriting the router cache layer in Rust and
                writing far too much about cold starts.
              </p>
              <p className="mt-3 flex items-center gap-1.5 text-xs text-slate-500 dark:text-slate-400">
                <svg
                  aria-hidden="true"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="1.75"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  className="h-3.5 w-3.5"
                >
                  <path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" />
                  <circle cx="12" cy="10" r="3" />
                </svg>
                Lisbon, Portugal · UTC+1 · usually online 09:00–18:00
              </p>
            </div>

            <dl className="mt-5 grid grid-cols-3 divide-x divide-slate-200 rounded-2xl border border-slate-200 bg-slate-50 py-3 dark:divide-slate-800 dark:border-slate-800 dark:bg-slate-800/40">
              {[
                { label: "Followers", value: followers.toLocaleString("en-US") },
                { label: "Repos", value: "38" },
                { label: "Merged", value: "1,204" },
              ].map((stat) => (
                <div key={stat.label} className="px-2 text-center">
                  <dt className="text-[11px] tracking-wide text-slate-500 uppercase dark:text-slate-400">
                    {stat.label}
                  </dt>
                  <dd className="mt-0.5 text-sm font-semibold tabular-nums text-slate-900 dark:text-slate-100">
                    {stat.value}
                  </dd>
                </div>
              ))}
            </dl>

            <div
              role="tablist"
              aria-label="Profile details"
              className="mt-5 flex gap-1 rounded-full bg-slate-100 p-1 dark:bg-slate-800"
            >
              {TABS.map((t) => {
                const active = t.id === tab;
                return (
                  <button
                    key={t.id}
                    ref={(el) => {
                      tabRefs.current[t.id] = el;
                    }}
                    role="tab"
                    type="button"
                    id={`${baseId}-tab-${t.id}`}
                    aria-selected={active}
                    aria-controls={`${baseId}-panel-${t.id}`}
                    tabIndex={active ? 0 : -1}
                    onClick={() => setTab(t.id)}
                    onKeyDown={onTabKeyDown}
                    className="relative flex-1 rounded-full px-3 py-1.5 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 focus-visible:outline-none dark:focus-visible:ring-offset-slate-800"
                  >
                    {active ? (
                      <motion.span
                        layoutId={`${baseId}-pill`}
                        transition={
                          reduce ? { duration: 0 } : { type: "spring", stiffness: 420, damping: 34 }
                        }
                        className="absolute inset-0 rounded-full bg-white shadow-sm dark:bg-slate-950"
                      />
                    ) : null}
                    <span
                      className={
                        active
                          ? "relative text-slate-900 dark:text-slate-50"
                          : "relative text-slate-500 dark:text-slate-400"
                      }
                    >
                      {t.label}
                    </span>
                  </button>
                );
              })}
            </div>

            <div className="mt-4 min-h-[188px]">
              <AnimatePresence mode="wait" initial={false}>
                <motion.div
                  key={tab}
                  initial={reduce ? false : { opacity: 0, y: 6 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={reduce ? { opacity: 1 } : { opacity: 0, y: -6 }}
                  transition={{ duration: 0.22, ease: "easeOut" }}
                  role="tabpanel"
                  id={`${baseId}-panel-${tab}`}
                  aria-labelledby={`${baseId}-tab-${tab}`}
                  tabIndex={0}
                  className="rounded-xl focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:outline-none"
                >
                  {tab === "activity" ? (
                    <ul className="space-y-3">
                      {ACTIVITY.map((row) => (
                        <li key={row.repo} className="flex gap-3">
                          <span
                            aria-hidden="true"
                            className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-violet-500"
                          />
                          <div className="min-w-0">
                            <p className="truncate text-xs font-medium text-slate-900 dark:text-slate-100">
                              {row.repo}
                            </p>
                            <p className="mt-0.5 text-xs leading-relaxed text-slate-500 dark:text-slate-400">
                              {row.action}
                            </p>
                            <p className="mt-1 flex items-center gap-2 text-[11px] tabular-nums">
                              <span className="text-slate-400 dark:text-slate-500">{row.when}</span>
                              {row.additions > 0 ? (
                                <span className="text-emerald-600 dark:text-emerald-400">
                                  +{row.additions}
                                </span>
                              ) : null}
                              {row.deletions > 0 ? (
                                <span className="text-rose-600 dark:text-rose-400">
                                  −{row.deletions}
                                </span>
                              ) : null}
                            </p>
                          </div>
                        </li>
                      ))}
                    </ul>
                  ) : null}

                  {tab === "stack" ? (
                    <ul className="space-y-3.5">
                      {STACK.map((item) => (
                        <li key={item.name}>
                          <div className="flex items-baseline justify-between gap-3">
                            <span className="text-xs font-medium text-slate-900 dark:text-slate-100">
                              {item.name}
                            </span>
                            <span className="text-[11px] text-slate-400 dark:text-slate-500">
                              {item.note}
                            </span>
                          </div>
                          <div
                            role="meter"
                            aria-label={`${item.name} proficiency`}
                            aria-valuenow={item.level}
                            aria-valuemin={0}
                            aria-valuemax={100}
                            className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800"
                          >
                            <motion.div
                              initial={reduce ? false : { width: 0 }}
                              animate={{ width: `${item.level}%` }}
                              transition={{ duration: 0.5, ease: "easeOut" }}
                              className="h-full rounded-full bg-gradient-to-r from-indigo-500 to-violet-500"
                            />
                          </div>
                        </li>
                      ))}
                    </ul>
                  ) : null}

                  {tab === "hours" ? (
                    <div>
                      <p className="text-xs text-slate-500 dark:text-slate-400">
                        Commits by hour, last 30 days. Peak at 5pm ({peak} commits).
                      </p>
                      <div className="mt-4 flex h-28 items-end gap-1.5">
                        {HOURS.map((value, i) => (
                          <div key={HOUR_LABELS[i]} className="flex flex-1 flex-col items-center gap-1.5">
                            <motion.div
                              initial={reduce ? false : { height: 0 }}
                              animate={{ height: `${Math.max((value / peak) * 100, 4)}%` }}
                              transition={{
                                duration: 0.4,
                                delay: reduce ? 0 : i * 0.03,
                                ease: "easeOut",
                              }}
                              title={`${HOUR_LABELS[i]}: ${value} commits`}
                              className="w-full rounded-sm bg-sky-400 dark:bg-sky-500"
                            />
                            <span className="text-[9px] text-slate-400 tabular-nums dark:text-slate-500">
                              {HOUR_LABELS[i]}
                            </span>
                          </div>
                        ))}
                      </div>
                      <p className="sr-only">
                        {HOURS.map((v, i) => `${HOUR_LABELS[i]}: ${v} commits`).join(", ")}
                      </p>
                    </div>
                  ) : null}
                </motion.div>
              </AnimatePresence>
            </div>

            <div className="mt-5 flex items-center justify-between border-t border-slate-200 pt-4 dark:border-slate-800">
              <span className="inline-flex items-center gap-1.5 rounded-full bg-emerald-50 px-2.5 py-1 text-[11px] font-medium text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400">
                <span aria-hidden="true" className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
                Open to mentoring
              </span>
              <a
                href="https://example.com/nadia-brar"
                target="_blank"
                rel="noopener"
                className="inline-flex items-center gap-1 rounded-md text-xs font-medium text-indigo-600 hover:text-indigo-500 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white focus-visible:outline-none dark:text-indigo-400 dark:focus-visible:ring-offset-slate-900"
              >
                Full profile
                <svg
                  aria-hidden="true"
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="2"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  className="h-3 w-3"
                >
                  <path d="M7 17 17 7M9 7h8v8" />
                </svg>
              </a>
            </div>
          </div>
        </motion.article>

        <p aria-live="polite" className="sr-only">
          {copied ? "Handle @nadia.brar copied to clipboard" : ""}
        </p>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →