diff --git a/src/components/nostr/calendar/CalendarStatusBadge.tsx b/src/components/nostr/calendar/CalendarStatusBadge.tsx
new file mode 100644
index 0000000..a42ffc9
--- /dev/null
+++ b/src/components/nostr/calendar/CalendarStatusBadge.tsx
@@ -0,0 +1,71 @@
+import type { LucideIcon } from "lucide-react";
+import { Clock, CheckCircle, CalendarDays, CalendarClock } from "lucide-react";
+import { cn } from "@/lib/utils";
+import type { CalendarEventStatus } from "@/lib/calendar-event";
+
+interface StatusConfig {
+ label: string;
+ className: string;
+ icon: LucideIcon;
+}
+
+const STATUS_CONFIG: Record<
+ CalendarEventStatus,
+ { label: string; className: string }
+> = {
+ upcoming: { label: "upcoming", className: "text-blue-500" },
+ ongoing: { label: "now", className: "text-green-500" },
+ past: { label: "past", className: "text-muted-foreground" },
+};
+
+interface CalendarStatusBadgeProps {
+ status: CalendarEventStatus;
+ /** Icon variant - use CalendarDays for date events, CalendarClock for time events */
+ variant?: "date" | "time";
+ /** Size variant - sm for feed, md for detail views */
+ size?: "sm" | "md";
+}
+
+/**
+ * Status badge for calendar events (NIP-52)
+ * Displays the current status (upcoming/now/past) with an appropriate icon
+ */
+export function CalendarStatusBadge({
+ status,
+ variant = "date",
+ size = "sm",
+}: CalendarStatusBadgeProps) {
+ const baseConfig = STATUS_CONFIG[status];
+ const OngoingIcon = variant === "time" ? CalendarClock : CalendarDays;
+
+ const config: StatusConfig = {
+ ...baseConfig,
+ icon:
+ status === "ongoing"
+ ? OngoingIcon
+ : status === "upcoming"
+ ? Clock
+ : CheckCircle,
+ };
+
+ const Icon = config.icon;
+
+ const sizeClasses = {
+ sm: { text: "text-xs", icon: "w-3 h-3", gap: "gap-1" },
+ md: { text: "text-sm", icon: "w-4 h-4", gap: "gap-1" },
+ }[size];
+
+ return (
+
+
+ {config.label}
+
+ );
+}
diff --git a/src/components/nostr/kinds/CalendarDateEventDetailRenderer.tsx b/src/components/nostr/kinds/CalendarDateEventDetailRenderer.tsx
index ace3e00..48d464f 100644
--- a/src/components/nostr/kinds/CalendarDateEventDetailRenderer.tsx
+++ b/src/components/nostr/kinds/CalendarDateEventDetailRenderer.tsx
@@ -3,58 +3,12 @@ import {
parseDateCalendarEvent,
getDateEventStatus,
formatDateRange,
- type CalendarEventStatus,
} from "@/lib/calendar-event";
import { UserName } from "../UserName";
import { MarkdownContent } from "../MarkdownContent";
+import { CalendarStatusBadge } from "../calendar/CalendarStatusBadge";
import { Label } from "@/components/ui/label";
-import {
- CalendarDays,
- MapPin,
- Users,
- Clock,
- CheckCircle,
- Hash,
- ExternalLink,
-} from "lucide-react";
-import { cn } from "@/lib/utils";
-
-/**
- * Status badge for calendar events
- */
-function CalendarStatusBadge({ status }: { status: CalendarEventStatus }) {
- const config = {
- upcoming: {
- label: "upcoming",
- className: "text-blue-500",
- icon: Clock,
- },
- ongoing: {
- label: "now",
- className: "text-green-500",
- icon: CalendarDays,
- },
- past: {
- label: "past",
- className: "text-muted-foreground",
- icon: CheckCircle,
- },
- }[status];
-
- const Icon = config.icon;
-
- return (
-
-
- {config.label}
-
- );
-}
+import { MapPin, Users, Hash, ExternalLink } from "lucide-react";
/**
* Detail renderer for Kind 31922 - Date-Based Calendar Event
@@ -75,7 +29,7 @@ export function CalendarDateEventDetailRenderer({
{/* Title */}
- {parsed.title || "Untitled Event"}
+ {parsed.title || parsed.identifier}
{/* Date and Status: date left, badge right */}
@@ -83,7 +37,7 @@ export function CalendarDateEventDetailRenderer({
{dateRange && (
{dateRange}
)}
-
+
{/* Organizer */}
diff --git a/src/components/nostr/kinds/CalendarDateEventRenderer.tsx b/src/components/nostr/kinds/CalendarDateEventRenderer.tsx
index a4a3865..cd0fb4c 100644
--- a/src/components/nostr/kinds/CalendarDateEventRenderer.tsx
+++ b/src/components/nostr/kinds/CalendarDateEventRenderer.tsx
@@ -2,53 +2,15 @@ import {
parseDateCalendarEvent,
getDateEventStatus,
formatDateRange,
- type CalendarEventStatus,
} from "@/lib/calendar-event";
import {
BaseEventContainer,
ClickableEventTitle,
type BaseEventProps,
} from "./BaseEventRenderer";
+import { CalendarStatusBadge } from "../calendar/CalendarStatusBadge";
import { Label } from "@/components/ui/label";
-import { CalendarDays, MapPin, Users, Clock, CheckCircle } from "lucide-react";
-import { cn } from "@/lib/utils";
-
-/**
- * Status badge for calendar events
- */
-function CalendarStatusBadge({ status }: { status: CalendarEventStatus }) {
- const config = {
- upcoming: {
- label: "upcoming",
- className: "text-blue-500",
- icon: Clock,
- },
- ongoing: {
- label: "now",
- className: "text-green-500",
- icon: CalendarDays,
- },
- past: {
- label: "past",
- className: "text-muted-foreground",
- icon: CheckCircle,
- },
- }[status];
-
- const Icon = config.icon;
-
- return (
-
-
- {config.label}
-
- );
-}
+import { MapPin, Users } from "lucide-react";
/**
* Renderer for Kind 31922 - Date-Based Calendar Event
@@ -67,7 +29,7 @@ export function CalendarDateEventRenderer({ event }: BaseEventProps) {
event={event}
className="text-lg font-semibold text-foreground"
>
- {parsed.title || "Untitled Event"}
+ {parsed.title || parsed.identifier}
{/* Date and status: time left, badge right */}
@@ -75,7 +37,7 @@ export function CalendarDateEventRenderer({ event }: BaseEventProps) {
{dateRange && (
{dateRange}
)}
-
+
{/* Description preview */}
diff --git a/src/components/nostr/kinds/CalendarTimeEventDetailRenderer.tsx b/src/components/nostr/kinds/CalendarTimeEventDetailRenderer.tsx
index bc12390..e3c088e 100644
--- a/src/components/nostr/kinds/CalendarTimeEventDetailRenderer.tsx
+++ b/src/components/nostr/kinds/CalendarTimeEventDetailRenderer.tsx
@@ -3,59 +3,12 @@ import {
parseTimeCalendarEvent,
getTimeEventStatus,
formatTimeRange,
- type CalendarEventStatus,
} from "@/lib/calendar-event";
import { UserName } from "../UserName";
import { MarkdownContent } from "../MarkdownContent";
+import { CalendarStatusBadge } from "../calendar/CalendarStatusBadge";
import { Label } from "@/components/ui/label";
-import {
- CalendarClock,
- MapPin,
- Users,
- Clock,
- CheckCircle,
- Hash,
- ExternalLink,
- Globe,
-} from "lucide-react";
-import { cn } from "@/lib/utils";
-
-/**
- * Status badge for calendar events
- */
-function CalendarStatusBadge({ status }: { status: CalendarEventStatus }) {
- const config = {
- upcoming: {
- label: "upcoming",
- className: "text-blue-500",
- icon: Clock,
- },
- ongoing: {
- label: "now",
- className: "text-green-500",
- icon: CalendarClock,
- },
- past: {
- label: "past",
- className: "text-muted-foreground",
- icon: CheckCircle,
- },
- }[status];
-
- const Icon = config.icon;
-
- return (
-
-
- {config.label}
-
- );
-}
+import { MapPin, Users, Hash, ExternalLink, Globe } from "lucide-react";
/**
* Detail renderer for Kind 31923 - Time-Based Calendar Event
@@ -81,7 +34,7 @@ export function CalendarTimeEventDetailRenderer({
{/* Title */}
- {parsed.title || "Untitled Event"}
+ {parsed.title || parsed.identifier}
{/* Time and Status: time left, badge right */}
@@ -89,7 +42,7 @@ export function CalendarTimeEventDetailRenderer({
{timeRange && (
{timeRange}
)}
-
+
{/* Timezone indicator */}
diff --git a/src/components/nostr/kinds/CalendarTimeEventRenderer.tsx b/src/components/nostr/kinds/CalendarTimeEventRenderer.tsx
index 524ce52..968420b 100644
--- a/src/components/nostr/kinds/CalendarTimeEventRenderer.tsx
+++ b/src/components/nostr/kinds/CalendarTimeEventRenderer.tsx
@@ -2,53 +2,15 @@ import {
parseTimeCalendarEvent,
getTimeEventStatus,
formatTimeRange,
- type CalendarEventStatus,
} from "@/lib/calendar-event";
import {
BaseEventContainer,
ClickableEventTitle,
type BaseEventProps,
} from "./BaseEventRenderer";
+import { CalendarStatusBadge } from "../calendar/CalendarStatusBadge";
import { Label } from "@/components/ui/label";
-import { CalendarClock, MapPin, Users, Clock, CheckCircle } from "lucide-react";
-import { cn } from "@/lib/utils";
-
-/**
- * Status badge for calendar events
- */
-function CalendarStatusBadge({ status }: { status: CalendarEventStatus }) {
- const config = {
- upcoming: {
- label: "upcoming",
- className: "text-blue-500",
- icon: Clock,
- },
- ongoing: {
- label: "now",
- className: "text-green-500",
- icon: CalendarClock,
- },
- past: {
- label: "past",
- className: "text-muted-foreground",
- icon: CheckCircle,
- },
- }[status];
-
- const Icon = config.icon;
-
- return (
-
-
- {config.label}
-
- );
-}
+import { MapPin, Users } from "lucide-react";
/**
* Renderer for Kind 31923 - Time-Based Calendar Event
@@ -72,7 +34,7 @@ export function CalendarTimeEventRenderer({ event }: BaseEventProps) {
event={event}
className="text-lg font-semibold text-foreground"
>
- {parsed.title || "Untitled Event"}
+ {parsed.title || parsed.identifier}
{/* Time and status: time left, badge right */}
@@ -80,7 +42,7 @@ export function CalendarTimeEventRenderer({ event }: BaseEventProps) {
{timeRange && (
{timeRange}
)}
-
+
{/* Description preview */}
diff --git a/src/lib/calendar-event.ts b/src/lib/calendar-event.ts
index 974d64c..5ee6876 100644
--- a/src/lib/calendar-event.ts
+++ b/src/lib/calendar-event.ts
@@ -1,5 +1,5 @@
import type { NostrEvent } from "@/types/nostr";
-import { getTagValue } from "applesauce-core/helpers";
+import { getTagValue, getOrComputeCachedValue } from "applesauce-core/helpers";
/**
* Participant in a calendar event (NIP-52)
@@ -14,7 +14,6 @@ export interface CalendarParticipant {
* Parsed Date-Based Calendar Event (kind 31922)
*/
export interface ParsedDateCalendarEvent {
- event: NostrEvent;
identifier: string;
title: string;
start: string; // YYYY-MM-DD
@@ -31,7 +30,6 @@ export interface ParsedDateCalendarEvent {
* Parsed Time-Based Calendar Event (kind 31923)
*/
export interface ParsedTimeCalendarEvent {
- event: NostrEvent;
identifier: string;
title: string;
start: number; // Unix timestamp
@@ -51,6 +49,10 @@ export interface ParsedTimeCalendarEvent {
*/
export type CalendarEventStatus = "upcoming" | "ongoing" | "past";
+// Caching symbols for parsed calendar events
+const ParsedDateCalendarEventSymbol = Symbol("ParsedDateCalendarEvent");
+const ParsedTimeCalendarEventSymbol = Symbol("ParsedTimeCalendarEvent");
+
/**
* Get all values for a given tag name
*/
@@ -74,12 +76,12 @@ function parseParticipants(event: NostrEvent): CalendarParticipant[] {
/**
* Parse a kind 31922 Date-Based Calendar Event
+ * Results are cached on the event object for performance
*/
export function parseDateCalendarEvent(
event: NostrEvent,
): ParsedDateCalendarEvent {
- return {
- event,
+ return getOrComputeCachedValue(event, ParsedDateCalendarEventSymbol, () => ({
identifier: getTagValue(event, "d") || "",
title: getTagValue(event, "title") || "",
start: getTagValue(event, "start") || "",
@@ -90,33 +92,35 @@ export function parseDateCalendarEvent(
participants: parseParticipants(event),
hashtags: getTagValues(event, "t"),
references: getTagValues(event, "r"),
- };
+ }));
}
/**
* Parse a kind 31923 Time-Based Calendar Event
+ * Results are cached on the event object for performance
*/
export function parseTimeCalendarEvent(
event: NostrEvent,
): ParsedTimeCalendarEvent {
- const startStr = getTagValue(event, "start");
- const endStr = getTagValue(event, "end");
+ return getOrComputeCachedValue(event, ParsedTimeCalendarEventSymbol, () => {
+ const startStr = getTagValue(event, "start");
+ const endStr = getTagValue(event, "end");
- return {
- event,
- identifier: getTagValue(event, "d") || "",
- title: getTagValue(event, "title") || "",
- start: startStr ? parseInt(startStr, 10) : 0,
- end: endStr ? parseInt(endStr, 10) : undefined,
- startTzid: getTagValue(event, "start_tzid") || undefined,
- endTzid: getTagValue(event, "end_tzid") || undefined,
- description: event.content || "",
- locations: getTagValues(event, "location"),
- geohash: getTagValue(event, "g") || undefined,
- participants: parseParticipants(event),
- hashtags: getTagValues(event, "t"),
- references: getTagValues(event, "r"),
- };
+ return {
+ identifier: getTagValue(event, "d") || "",
+ title: getTagValue(event, "title") || "",
+ start: startStr ? parseInt(startStr, 10) : 0,
+ end: endStr ? parseInt(endStr, 10) : undefined,
+ startTzid: getTagValue(event, "start_tzid") || undefined,
+ endTzid: getTagValue(event, "end_tzid") || undefined,
+ description: event.content || "",
+ locations: getTagValues(event, "location"),
+ geohash: getTagValue(event, "g") || undefined,
+ participants: parseParticipants(event),
+ hashtags: getTagValues(event, "t"),
+ references: getTagValues(event, "r"),
+ };
+ });
}
/**