mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-09 23:16:50 +02:00
refactor(calendar): extract shared components and add caching
- Extract CalendarStatusBadge to shared component with variant/size props - Add symbol-based caching to parseDateCalendarEvent and parseTimeCalendarEvent using applesauce's getOrComputeCachedValue for performance - Use 'd' tag (identifier) as title fallback instead of "Untitled Event" - Remove duplicate CalendarStatusBadge implementations from all 4 renderers - Remove unused imports (cn, CalendarDays, CalendarClock, Clock, CheckCircle)
This commit is contained in:
71
src/components/nostr/calendar/CalendarStatusBadge.tsx
Normal file
71
src/components/nostr/calendar/CalendarStatusBadge.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center flex-shrink-0",
|
||||
sizeClasses.text,
|
||||
sizeClasses.gap,
|
||||
config.className,
|
||||
)}
|
||||
>
|
||||
<Icon className={sizeClasses.icon} />
|
||||
<span>{config.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 text-sm flex-shrink-0",
|
||||
config.className,
|
||||
)}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span>{config.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import { MapPin, Users, Hash, ExternalLink } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Detail renderer for Kind 31922 - Date-Based Calendar Event
|
||||
@@ -75,7 +29,7 @@ export function CalendarDateEventDetailRenderer({
|
||||
<header className="flex flex-col gap-4 border-b border-border pb-6">
|
||||
{/* Title */}
|
||||
<h1 className="text-3xl font-bold">
|
||||
{parsed.title || "Untitled Event"}
|
||||
{parsed.title || parsed.identifier}
|
||||
</h1>
|
||||
|
||||
{/* Date and Status: date left, badge right */}
|
||||
@@ -83,7 +37,7 @@ export function CalendarDateEventDetailRenderer({
|
||||
{dateRange && (
|
||||
<span className="text-sm text-muted-foreground">{dateRange}</span>
|
||||
)}
|
||||
<CalendarStatusBadge status={status} />
|
||||
<CalendarStatusBadge status={status} variant="date" size="md" />
|
||||
</div>
|
||||
|
||||
{/* Organizer */}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 text-xs flex-shrink-0",
|
||||
config.className,
|
||||
)}
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
<span>{config.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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}
|
||||
</ClickableEventTitle>
|
||||
|
||||
{/* Date and status: time left, badge right */}
|
||||
@@ -75,7 +37,7 @@ export function CalendarDateEventRenderer({ event }: BaseEventProps) {
|
||||
{dateRange && (
|
||||
<span className="text-xs text-muted-foreground">{dateRange}</span>
|
||||
)}
|
||||
<CalendarStatusBadge status={status} />
|
||||
<CalendarStatusBadge status={status} variant="date" size="sm" />
|
||||
</div>
|
||||
|
||||
{/* Description preview */}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 text-sm flex-shrink-0",
|
||||
config.className,
|
||||
)}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<span>{config.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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({
|
||||
<header className="flex flex-col gap-4 border-b border-border pb-6">
|
||||
{/* Title */}
|
||||
<h1 className="text-3xl font-bold">
|
||||
{parsed.title || "Untitled Event"}
|
||||
{parsed.title || parsed.identifier}
|
||||
</h1>
|
||||
|
||||
{/* Time and Status: time left, badge right */}
|
||||
@@ -89,7 +42,7 @@ export function CalendarTimeEventDetailRenderer({
|
||||
{timeRange && (
|
||||
<span className="text-sm text-muted-foreground">{timeRange}</span>
|
||||
)}
|
||||
<CalendarStatusBadge status={status} />
|
||||
<CalendarStatusBadge status={status} variant="time" size="md" />
|
||||
</div>
|
||||
|
||||
{/* Timezone indicator */}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 text-xs flex-shrink-0",
|
||||
config.className,
|
||||
)}
|
||||
>
|
||||
<Icon className="w-3 h-3" />
|
||||
<span>{config.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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}
|
||||
</ClickableEventTitle>
|
||||
|
||||
{/* Time and status: time left, badge right */}
|
||||
@@ -80,7 +42,7 @@ export function CalendarTimeEventRenderer({ event }: BaseEventProps) {
|
||||
{timeRange && (
|
||||
<span className="text-xs text-muted-foreground">{timeRange}</span>
|
||||
)}
|
||||
<CalendarStatusBadge status={status} />
|
||||
<CalendarStatusBadge status={status} variant="time" size="sm" />
|
||||
</div>
|
||||
|
||||
{/* Description preview */}
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user