Web InnoventixFreeCode

Event Card

Original · free

An event card with a date badge and details.

byWeb InnoventixReact + Tailwind
cardeventcards
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/card-event.json
card-event.tsx
"use client";

import { useState } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";

type EventData = {
  id: string;
  month: string;
  day: string;
  weekday: string;
  title: string;
  time: string;
  venue: string;
  city: string;
  category: string;
  price: string;
  image: string;
  attendees: number;
  featured?: boolean;
};

const EVENTS: EventData[] = [
  {
    id: "reactconf",
    month: "OCT",
    day: "14",
    weekday: "Tuesday",
    title: "React Conf 2026: The Compiler Era",
    time: "9:00 AM – 6:00 PM PDT",
    venue: "Moscone West",
    city: "San Francisco, CA",
    category: "Conference",
    price: "$349",
    image: "/img/gallery/g07.webp",
    attendees: 1240,
    featured: true,
  },
  {
    id: "jazz",
    month: "NOV",
    day: "02",
    weekday: "Sunday",
    title: "Blue Note: Robert Glasper Trio",
    time: "8:30 PM – 11:00 PM EST",
    venue: "Blue Note Jazz Club",
    city: "New York, NY",
    category: "Live Music",
    price: "$65",
    image: "/img/gallery/g19.webp",
    attendees: 210,
  },
  {
    id: "workshop",
    month: "SEP",
    day: "27",
    weekday: "Saturday",
    title: "Hand-Thrown Ceramics Workshop",
    time: "10:00 AM – 1:00 PM",
    venue: "Clay & Kiln Studio",
    city: "Portland, OR",
    category: "Workshop",
    price: "Free",
    image: "/img/gallery/g31.webp",
    attendees: 24,
  },
];

function StatIcon({ path, className }: { path: string; className?: string }) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.8}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
      className={className}
    >
      <path d={path} />
    </svg>
  );
}

const ICON = {
  clock: "M12 7v5l3 2M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z",
  pin: "M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0ZM12 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z",
  users:
    "M17 20v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 10a4 4 0 1 0 0-8 4 4 0 0 0 0 8ZM23 20v-2a4 4 0 0 0-3-3.87M16 2.13a4 4 0 0 1 0 7.75",
  share:
    "M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13",
  bookmark: "M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16Z",
  check: "M20 6 9 17l-5-5",
  ticket:
    "M15 5v2M15 11v2M15 17v2M5 5h14a2 2 0 0 1 2 2v3a2 2 0 0 0 0 4v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3a2 2 0 0 0 0-4V7a2 2 0 0 1 2-2Z",
  calendar:
    "M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z",
};

function DateBadge({
  month,
  day,
  featured,
}: {
  month: string;
  day: string;
  featured?: boolean;
}) {
  return (
    <div
      className={[
        "flex w-14 shrink-0 flex-col items-center overflow-hidden rounded-xl text-center shadow-sm ring-1",
        featured
          ? "bg-indigo-600 text-white ring-indigo-500/40 dark:bg-indigo-500"
          : "bg-white text-slate-900 ring-slate-900/10 dark:bg-slate-800 dark:text-slate-50 dark:ring-white/10",
      ].join(" ")}
    >
      <span
        className={[
          "w-full py-0.5 text-[10px] font-bold uppercase tracking-widest",
          featured
            ? "bg-black/15 text-indigo-50"
            : "bg-rose-500 text-white",
        ].join(" ")}
      >
        {month}
      </span>
      <span className="py-1 text-2xl font-black leading-none tabular-nums">
        {day}
      </span>
    </div>
  );
}

function AttendeeStack({ count }: { count: number }) {
  const palette = ["bg-emerald-400", "bg-sky-400", "bg-amber-400", "bg-rose-400"];
  return (
    <div className="flex items-center gap-2">
      <div className="flex -space-x-2">
        {palette.map((c, i) => (
          <span
            key={i}
            className={`h-6 w-6 rounded-full ring-2 ring-white dark:ring-slate-900 ${c}`}
          />
        ))}
      </div>
      <span className="text-xs font-medium text-slate-500 dark:text-slate-400">
        {count.toLocaleString()} going
      </span>
    </div>
  );
}

function EventCard({ event }: { event: EventData }) {
  const reduce = useReducedMotion();
  const [going, setGoing] = useState(false);
  const [saved, setSaved] = useState(false);
  const [copied, setCopied] = useState(false);
  const [busy, setBusy] = useState(false);

  const liveCount = event.attendees + (going ? 1 : 0);

  async function handleShare() {
    try {
      await navigator.clipboard.writeText(
        `${event.title} — ${event.month} ${event.day}, ${event.venue}`
      );
      setCopied(true);
      window.setTimeout(() => setCopied(false), 1600);
    } catch {
      setCopied(false);
    }
  }

  function handleRsvp() {
    if (busy) return;
    setBusy(true);
    window.setTimeout(
      () => {
        setGoing((v) => !v);
        setBusy(false);
      },
      reduce ? 0 : 620
    );
  }

  return (
    <article className="group relative flex flex-col overflow-hidden rounded-2xl bg-white ring-1 ring-slate-900/10 transition-shadow duration-300 hover:shadow-xl hover:shadow-slate-900/10 dark:bg-slate-900 dark:ring-white/10 dark:hover:shadow-black/40">
      <div className="relative aspect-[16/10] w-full overflow-hidden">
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img
          src={event.image}
          alt={`${event.title} event cover`}
          loading="lazy"
          draggable={false}
          className={[
            "h-full w-full object-cover transition-transform duration-[900ms] ease-out",
            reduce ? "" : "group-hover:scale-105",
          ].join(" ")}
        />
        <div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/55 via-black/5 to-transparent" />

        <span className="absolute left-3 top-3 inline-flex items-center gap-1.5 rounded-full bg-black/45 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wide text-white backdrop-blur">
          <StatIcon path={ICON.ticket} className="h-3.5 w-3.5" />
          {event.category}
        </span>

        <button
          type="button"
          onClick={() => setSaved((v) => !v)}
          aria-pressed={saved}
          aria-label={saved ? "Remove from saved events" : "Save event"}
          className="absolute right-3 top-3 inline-flex h-9 w-9 items-center justify-center rounded-full bg-white/90 text-slate-700 shadow-sm outline-none ring-1 ring-black/5 backdrop-blur transition-all hover:scale-105 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 active:scale-95 dark:bg-slate-800/90 dark:text-slate-200"
        >
          <StatIcon
            path={ICON.bookmark}
            className={[
              "h-[18px] w-[18px] transition-colors",
              saved ? "fill-rose-500 text-rose-500" : "fill-transparent",
            ].join(" ")}
          />
        </button>

        <div className="absolute bottom-3 left-3 flex items-end gap-3">
          <DateBadge
            month={event.month}
            day={event.day}
            featured={event.featured}
          />
          <div className="pb-0.5">
            <p className="text-[11px] font-semibold uppercase tracking-wider text-white/80">
              {event.weekday}
            </p>
            <p className="text-sm font-bold text-white drop-shadow">
              {event.price === "Free" ? "Free entry" : `From ${event.price}`}
            </p>
          </div>
        </div>
      </div>

      <div className="flex flex-1 flex-col gap-4 p-5">
        <h3 className="text-lg font-bold leading-snug tracking-tight text-slate-900 dark:text-slate-50">
          {event.title}
        </h3>

        <dl className="space-y-2 text-sm">
          <div className="flex items-center gap-2.5 text-slate-600 dark:text-slate-300">
            <StatIcon
              path={ICON.clock}
              className="h-4 w-4 shrink-0 text-indigo-500 dark:text-indigo-400"
            />
            <dd>{event.time}</dd>
          </div>
          <div className="flex items-center gap-2.5 text-slate-600 dark:text-slate-300">
            <StatIcon
              path={ICON.pin}
              className="h-4 w-4 shrink-0 text-indigo-500 dark:text-indigo-400"
            />
            <dd>
              <span className="font-medium text-slate-800 dark:text-slate-100">
                {event.venue}
              </span>
              <span className="text-slate-400 dark:text-slate-500"> · {event.city}</span>
            </dd>
          </div>
        </dl>

        <div className="mt-auto flex items-center justify-between gap-3 border-t border-slate-100 pt-4 dark:border-white/10">
          <AttendeeStack count={liveCount} />

          <div className="flex items-center gap-2">
            <div className="relative">
              <button
                type="button"
                onClick={handleShare}
                aria-label="Copy event details to clipboard"
                className="inline-flex h-9 w-9 items-center justify-center rounded-lg text-slate-500 outline-none ring-1 ring-slate-900/10 transition-colors hover:bg-slate-50 hover:text-slate-800 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:text-slate-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-slate-100 dark:focus-visible:ring-offset-slate-900"
              >
                <StatIcon
                  path={copied ? ICON.check : ICON.share}
                  className={[
                    "h-4 w-4",
                    copied ? "text-emerald-500" : "",
                  ].join(" ")}
                />
              </button>
              <AnimatePresence>
                {copied ? (
                  <motion.span
                    initial={reduce ? false : { opacity: 0, y: 4 }}
                    animate={{ opacity: 1, y: 0 }}
                    exit={reduce ? { opacity: 0 } : { opacity: 0, y: 4 }}
                    className="absolute -top-8 right-0 whitespace-nowrap rounded-md bg-slate-900 px-2 py-1 text-[11px] font-medium text-white shadow-lg dark:bg-slate-700"
                  >
                    Copied
                  </motion.span>
                ) : null}
              </AnimatePresence>
            </div>

            <button
              type="button"
              onClick={handleRsvp}
              aria-pressed={going}
              disabled={busy}
              className={[
                "inline-flex min-w-[104px] items-center justify-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold outline-none transition-all focus-visible:ring-2 focus-visible:ring-offset-2 active:scale-[0.97] disabled:cursor-wait dark:focus-visible:ring-offset-slate-900",
                going
                  ? "bg-emerald-500 text-white ring-1 ring-emerald-500 hover:bg-emerald-600 focus-visible:ring-emerald-500"
                  : "bg-indigo-600 text-white hover:bg-indigo-700 focus-visible:ring-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400",
              ].join(" ")}
            >
              {busy ? (
                <svg
                  viewBox="0 0 24 24"
                  className="ce-spin h-4 w-4"
                  aria-hidden="true"
                  fill="none"
                >
                  <circle
                    cx="12"
                    cy="12"
                    r="9"
                    stroke="currentColor"
                    strokeWidth="3"
                    className="opacity-25"
                  />
                  <path
                    d="M21 12a9 9 0 0 0-9-9"
                    stroke="currentColor"
                    strokeWidth="3"
                    strokeLinecap="round"
                  />
                </svg>
              ) : going ? (
                <>
                  <StatIcon path={ICON.check} className="h-4 w-4" />
                  Going
                </>
              ) : (
                "RSVP"
              )}
            </button>
          </div>
        </div>
      </div>
    </article>
  );
}

export default function CardEvent() {
  return (
    <section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-20 dark:bg-slate-950">
      <style>{`
        @keyframes ce-spin { to { transform: rotate(360deg); } }
        .ce-spin { animation: ce-spin 0.7s linear infinite; }
        @media (prefers-reduced-motion: reduce) {
          .ce-spin { animation-duration: 0.01ms; animation-iteration-count: 1; }
        }
      `}</style>

      <div className="mx-auto max-w-6xl">
        <header className="mb-10 flex flex-col gap-3 sm:mb-14">
          <span className="inline-flex w-fit items-center gap-1.5 rounded-full bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-indigo-600 ring-1 ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/20">
            <StatIcon path={ICON.calendar} className="h-3.5 w-3.5" />
            Upcoming
          </span>
          <h2 className="text-3xl font-black tracking-tight text-slate-900 sm:text-4xl dark:text-slate-50">
            Events near you
          </h2>
          <p className="max-w-xl text-base text-slate-500 dark:text-slate-400">
            RSVP, save for later, and share the ones worth showing up for. Tap a
            card to lock in your spot.
          </p>
        </header>

        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {EVENTS.map((event) => (
            <EventCard key={event.id} event={event} />
          ))}
        </div>
      </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 →