Future of Open Source: Annual Summit
Two days of talks, workshops, and hallway conversations with maintainers from across the open-source ecosystem.
- July 1, 2026
- 09:00 - 18:00
- Istanbul Conference Center
Event preview card with six status states and four layouts — capacity-aware badges, overlay links, and soft-failure item handling.
Use when listing events on a community / association / conference / training site, or when mixing events into a multi-content feed. The grid variant fits magazine grids; the feed variant fits social-style single-column feeds; the list variant fits info-rich sidebars / dashboard listings; the compact variant fits text-only sidebar widgets / 'upcoming events' tickers. Public helpers (`getEventStatus`, `EVENT_STATUS_CONFIG`, `formatEventDate`, `getDaysUntilEvent`) are exported alongside the card so consumers can derive status independently for header counters, calendar coloring, filter logic, or deterministic tests — without rendering a card. Migration origin: kasder kas-social-front-v0 EventCard.tsx + SocialEventCard.tsx.
pnpm dlx shadcn@latest initpnpm dlx shadcn@latest add @ilinxa/event-cardAdd -fixtures for dummy data:
pnpm dlx shadcn@latest add @ilinxa/event-card-fixturesCLI can't resolve @ilinxa? The namespace is listed in the official shadcn registry directory, so current CLIs need no configuration. If yours can't resolve it (older or pinned versions, self-hosted mirrors), register it manually in components.json:
"registries": {
"@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}Two days of talks, workshops, and hallway conversations with maintainers from across the open-source ecosystem.
Hands-on workshop covering Tailwind v4's new theme system, container queries, and OKLCH color migration.
Walk through a real WCAG 2.1 AA audit on a production component library and the fixes that followed.
Live coding session — building a force-directed knowledge graph editor on top of Sigma.js v3 and React 19.
Five founders share what's working and what's not in technical hiring this year. Off the record.
Two-day hands-on training on GraphQL federation v2 — from subgraph composition through production rollout.
Full-day flagship event — keynotes, breakout tracks, networking, and the annual Knowledge Day awards ceremony.
Casual drop-in office hours — no registration needed, no cap. Bring your questions about anything we ship.
"use client"; import { useState } from "react";import { Bookmark, BookmarkCheck, CalendarPlus, Share2 } from "lucide-react";import { Button } from "@/components/ui/button";import { Tabs, TabsContent, TabsTrigger } from "@/components/ui/tabs";import { SwipeTabsList } from "@/components/site/swipe-tabs-list";import { EventCard } from "./event-card";import { dummyCustomTypeStyles, dummyEvents, dummyNow, dummyTrEvents, dummyTrLabels, dummyTrTypeStyles, dummyTypeStyles, formatDateTr,} from "./dummy-data";import type { EventCardItem } from "./types"; const HREF_BASE = "/events"; function makeHref(event: EventCardItem) { return `${HREF_BASE}/${event.id}`;} function ActionsCluster({ eventId, saved, onToggle,}: { eventId: string; saved: boolean; onToggle: (id: string) => void;}) { return ( <div className="flex gap-1.5"> <Button variant="secondary" size="sm" onClick={(e) => { e.preventDefault(); e.stopPropagation(); onToggle(eventId); }} aria-label={saved ? "Remove from saved events" : "Save event"} > {saved ? ( <BookmarkCheck aria-hidden="true" className="size-4" /> ) : ( <Bookmark aria-hidden="true" className="size-4" /> )} </Button> <Button variant="secondary" size="sm" onClick={(e) => { e.preventDefault(); e.stopPropagation(); }} aria-label="Add to calendar" > <CalendarPlus aria-hidden="true" className="size-4" /> </Button> <Button variant="secondary" size="sm" onClick={(e) => { e.preventDefault(); e.stopPropagation(); }} aria-label="Share event" > <Share2 aria-hidden="true" className="size-4" /> </Button> </div> );} const feedSlice = dummyEvents.filter((e) => ["evt-open", "evt-ongoing", "evt-upcoming", "evt-lastspots"].includes(e.id),); const featuredEvent = dummyEvents.find((e) => e.id === "evt-featured")!;const openEvent = dummyEvents.find((e) => e.id === "evt-open")!; export default function EventCardDemo() { const [saved, setSaved] = useState<Set<string>>(() => new Set()); const toggleSaved = (id: string) => setSaved((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); return ( <Tabs defaultValue="grid" className="w-full"> <SwipeTabsList> <TabsTrigger value="grid">Grid</TabsTrigger> <TabsTrigger value="feed">Feed</TabsTrigger> <TabsTrigger value="list">List</TabsTrigger> <TabsTrigger value="compact">Compact</TabsTrigger> <TabsTrigger value="featured">Featured</TabsTrigger> <TabsTrigger value="localized">Localized (TR)</TabsTrigger> <TabsTrigger value="custom-types">Custom types</TabsTrigger> <TabsTrigger value="actions">Actions slot</TabsTrigger> </SwipeTabsList> {/* 1. Grid */} <TabsContent value="grid" className="mt-6"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> {dummyEvents.map((event) => ( <EventCard key={event.id} event={event} variant="grid" href={makeHref(event)} now={dummyNow} typeStyles={dummyTypeStyles} /> ))} </div> </TabsContent> {/* 2. Feed */} <TabsContent value="feed" className="mt-6"> <div className="flex flex-col gap-4 max-w-3xl mx-auto"> {feedSlice.map((event) => ( <EventCard key={event.id} event={event} variant="feed" href={makeHref(event)} now={dummyNow} typeStyles={dummyTypeStyles} /> ))} </div> </TabsContent> {/* 3. List — dense info-rich rows */} <TabsContent value="list" className="mt-6"> <p className="mb-3 text-sm text-muted-foreground"> List variant — dense info-rich row with status + 4-icon meta line (date / time / location / spots-left) + status-aware right indicator (days-until count for upcoming, "Live now" pulsing pill for ongoing, chevron for ended). Featured events get a left accent border. </p> <div className="rounded-2xl border border-border/50 bg-card max-w-3xl mx-auto"> {dummyEvents.map((event) => ( <EventCard key={event.id} event={event} variant="list" href={makeHref(event)} now={dummyNow} typeStyles={dummyTypeStyles} /> ))} </div> </TabsContent> {/* 4. Compact — text-only minimal rows for sidebars / widgets */} <TabsContent value="compact" className="mt-6"> <div className="grid md:grid-cols-2 gap-8"> <div> <p className="mb-3 text-sm text-muted-foreground"> Compact variant — text-only minimal row for sidebars / widgets. No thumbnail, no status badge, no capacity bar — title + type pill + 3 stacked meta lines (date / time / location). Localized labels applied here to mirror the source pattern. </p> <div className="rounded-2xl border border-border/50 bg-card p-2 max-w-md"> {dummyTrEvents.slice(0, 4).map((event) => ( <EventCard key={event.id} event={event} variant="compact" href={makeHref(event)} now={dummyNow} typeStyles={dummyTrTypeStyles} labels={dummyTrLabels} formatDate={(d) => new Date(d).toLocaleDateString("tr-TR", { day: "numeric", month: "long", }) } /> ))} <div className="text-center pt-3 pb-2"> <a href="#" className="text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1" > Tüm Etkinlikler → </a> </div> </div> </div> <div> <p className="mb-3 text-sm text-muted-foreground"> Same compact variant in English with the default labels & full date format. </p> <div className="rounded-2xl border border-border/50 bg-card p-2 max-w-md"> {dummyEvents.slice(0, 4).map((event) => ( <EventCard key={event.id} event={event} variant="compact" href={makeHref(event)} now={dummyNow} typeStyles={dummyTypeStyles} /> ))} <div className="text-center pt-3 pb-2"> <a href="#" className="text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1" > All events → </a> </div> </div> </div> </div> </TabsContent> {/* 5. Featured — grid + feed side by side */} <TabsContent value="featured" className="mt-6"> <div className="space-y-8"> <div> <p className="mb-3 text-sm text-muted-foreground"> Grid variant — top accent border + star prefix on title. </p> <div className="grid md:grid-cols-2 gap-6"> <EventCard event={featuredEvent} variant="grid" href={makeHref(featuredEvent)} now={dummyNow} typeStyles={dummyTypeStyles} /> <EventCard event={openEvent} variant="grid" href={makeHref(openEvent)} now={dummyNow} typeStyles={dummyTypeStyles} /> </div> </div> <div> <p className="mb-3 text-sm text-muted-foreground"> Feed variant — inset ring + star prefix. </p> <div className="max-w-3xl mx-auto"> <EventCard event={featuredEvent} variant="feed" href={makeHref(featuredEvent)} now={dummyNow} typeStyles={dummyTypeStyles} /> </div> </div> </div> </TabsContent> {/* 4. Localized (TR) */} <TabsContent value="localized" className="mt-6"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> {dummyTrEvents.map((event) => ( <EventCard key={event.id} event={event} variant="grid" href={makeHref(event)} now={dummyNow} typeStyles={dummyTrTypeStyles} labels={dummyTrLabels} formatDate={formatDateTr} /> ))} </div> </TabsContent> {/* 5. Custom types */} <TabsContent value="custom-types" className="mt-6"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> {dummyEvents.map((event) => ( <EventCard key={event.id} event={event} variant="grid" href={makeHref(event)} now={dummyNow} typeStyles={dummyCustomTypeStyles} /> ))} </div> </TabsContent> {/* 6. Actions slot + custom href */} <TabsContent value="actions" className="mt-6"> <div className="space-y-2"> <p className="text-sm text-muted-foreground"> Action buttons stop propagation — clicks on Save / Calendar / Share don't navigate. The rest of the card surface still does. Clicked saved-state:{" "} <span className="font-mono text-xs"> {saved.size > 0 ? Array.from(saved).join(", ") : "(none)"} </span> </p> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6"> {dummyEvents.slice(0, 3).map((event) => ( <EventCard key={event.id} event={event} variant="grid" getHref={(e) => `/etkinlik/${e.id}`} now={dummyNow} typeStyles={dummyTypeStyles} actions={ <ActionsCluster eventId={event.id} saved={saved.has(event.id)} onToggle={toggleSaved} /> } /> ))} </div> </div> </TabsContent> </Tabs> );} Reach for EventCard when you need an event preview with a live, time-and-capacity-aware status (registration open, almost full, sold out, happening right now, ended) plus a status-driven CTA. Four layouts: variant="grid" (image-on-top magazine card), variant="feed" (full-bleed image background, white-on-dark content), variant="list" (thumbnail + 4-icon meta + status indicator), and variant="compact" (text-only minimal row for sidebars / widgets).
import { EventCard } from "@/components/event-card";
<EventCard
event={{
id: "1",
title: "Annual Summit",
type: "Conference",
date: "2026-07-01",
time: "09:00 - 18:00",
location: "Istanbul Conference Center",
image: "/cover.jpg",
capacity: 200,
registered: 50,
}}
variant="grid"
href="/events/1"
/>;grid — image-on-top magazine card with full meta lines, capacity progress bar, and decorative status CTA at the foot. Renders well in 1/2/3-column responsive grids.feed — full-bleed image background, content overlaid white-on-dark. Single-column feed item. No capacity progress bar (intentional — spots-left is shown inline in the meta row instead).list— info-rich row with thumbnail: 16/20 px square thumb left, content middle (status badge + type badge + title + 4-icon inline meta row showing date / time / location / spots-left), status-aware right indicator (days-until / pulsing "Live now" / chevron). Featured rows get a left accent border. Use when you want a scannable list with visual context per row.compact— text-only minimal row for sidebars / widgets / "upcoming events" tickers. Title + type pill (top-right) + 3 stacked meta lines (date / time / location). NO thumbnail, NO status badge, NO capacity bar. Tightest variant — maximum density per pixel. Pairs naturally with a "See all →" footer link.Status flows from the event data + an optional now reference. The card never needs you to set status — it's derived from the event and clock:
open — registrations available, more than 7 days out.upcoming — within 7 days. Amber badge.lastSpots — ≥80% capacity hit. Amber badge + spots-left counter flips to text-destructive when ≤5 left.ongoing — between start and end. Pulsing badge + live indicator.full — capacity reached. Sold-out CTA, button disabled.expired — past end date. Card fades to opacity-60 grayscale-30; CTA shows "View details."The status helpers are exported alongside the component so consumers can derive status, format dates, and compute days-until without rendering an actual card — for header counters, calendar day-cells, filter logic, deterministic tests:
import {
EventCard,
getEventStatus,
EVENT_STATUS_CONFIG,
formatEventDate,
getDaysUntilEvent,
type EventStatus,
} from "@/components/event-card";
// Header counter
const liveCount = events.filter(
(e) => getEventStatus(e) === "ongoing",
).length;
// Calendar day cell — pure-helper composition, no card render
function DayCell({ events, day }: { events: EventCardItem[]; day: Date }) {
const todays = events.filter((e) => sameDay(e.date, day));
const hasOngoing = todays.some(
(e) => getEventStatus(e, day) === "ongoing",
);
return (
<div className={hasOngoing ? "bg-accent/30" : ""}>
{todays.length > 0 && <span>{todays.length}</span>}
</div>
);
}import NextLink from "next/link";
<EventCard
event={event}
variant="grid"
linkComponent={NextLink}
getHref={(e) => `/etkinlik/${e.slug}`}
/>;<EventCard
event={event}
variant="grid"
href={href}
formatDate={(d) =>
new Date(d).toLocaleDateString("tr-TR", {
day: "numeric",
month: "long",
year: "numeric",
})
}
labels={{
open: "Kayıt Açık",
upcoming: "Yaklaşıyor",
ctaRegister: "Kayıt Ol",
daysUntilSuffix: "gün kaldı",
capacityAriaPrefix: "Kayıtlı",
capacityAriaSeparator: "/",
// ... 17 keys total, all optional
}}
typeStyles={{
Konferans: { className: "bg-primary/10 text-primary border-primary/20" },
}}
/>;The whole card is a link via the overlay-link pattern. Drop nested interactive buttons (Save / Share / Calendar) into the actions prop — they render at z-10 above the link overlay. Each action button MUST call e.preventDefault() + e.stopPropagation() in its onClick or the click bubbles to the link and navigates. Tab order: card-link → action1 → action2 → next card.
now for testing + live clocksPass a now Date to make status derivation deterministic. For demos / SSR / screenshot tests, pin nowto a fixed date so events don't silently drift over time. For minute-accurate live status, drive now from a parent setInterval:
function LiveEvents({ events }: { events: EventCardItem[] }) {
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 60_000);
return () => clearInterval(id);
}, []);
return events.map((e) => (
<EventCard key={e.id} event={e} variant="grid" href={`/events/${e.id}`} now={now} />
));
}Only id, title, type, date are required. Missing optional fields gracefully omit:
capacity / registered — capacity bar + spots-left counter omitted; status logic skips full / lastSpots states; CTA reverts to plain Register / View Details / Ended.image — tinted placeholder with calendar icon.endDate — falls back to date (single-day event spans 00:00 → 23:59:59).time / location / description — meta line omitted.actions.setInterval. For live updates, drive now from upstream.getEventStatus, EVENT_STATUS_CONFIG, formatEventDate, getDaysUntilEvent) are part of the API contract — stable across v0.x.motion-safe: — reduced-motion users see static cards.