diff --git a/src/components/views/eventsSmokesignal/calendarEvent.tsx b/src/components/views/eventsSmokesignal/calendarEvent.tsx new file mode 100644 index 0000000..985d044 --- /dev/null +++ b/src/components/views/eventsSmokesignal/calendarEvent.tsx @@ -0,0 +1,208 @@ +import { CollectionViewComponent, CollectionViewProps } from "../getView"; + +interface CalendarEventLexiconValue { + mode: string; + name: string; + uris: Array<{ + uri: string; + name: string; + $type: string; + }>; + $type: string; + status: string; + startsAt: string; + createdAt: string; + description?: string; + text?: string; + location?: { + name: string; + street: string; + locality: string; + region: string; + }; +} + +interface CalendarEventLexicon { + uri: string; + cid: string; + value: CalendarEventLexiconValue; +} + +const EventsSmokesignalCalendarEventView: CollectionViewComponent< + CollectionViewProps +> = ({ data }: CollectionViewProps) => { + const { value } = data as CalendarEventLexicon; + + // Helper function to format date strings + const formatDate = (dateString: string): string => { + try { + const date = new Date(dateString); + const friendly = getFriendlyUntilDate(date); + return ( + date.toLocaleString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "2-digit", + timeZoneName: "short", + }) + (friendly !== "expired" ? " - " + friendly : "") + ); + } catch (e) { + return "Invalid Date"; + } + }; + + // Helper function to extract the human-friendly part of status/mode + const getFriendlyTerm = (term: string): string => { + const parts = term.split("#"); + return parts.length > 1 + ? parts[1].charAt(0).toUpperCase() + parts[1].slice(1) + : term; + }; + + const statusColor = (status: string): string => { + if (status.includes("scheduled")) return "green"; + if (status.includes("cancelled")) return "red"; + if (status.includes("postponed")) return "orange"; + return "#555"; // Default color + }; + + return ( +
+

📅 {value.name}

+ +
+

+ Starts: {formatDate(value.startsAt)} +

+

+ Status: + + {getFriendlyTerm(value.status)} + +

+

+ Where: {getFriendlyTerm(value.mode)}{" "} + {value.location && + `${value.location.name}: ${value.location.street}, ${value.location.locality}, ${value.location.region}`} +

+
+ +
+

Description

+

+ {(value.description || value.text) + ?.split("\n") + .map((t) => (t == "\n" ?

:

{t}

))} +

+
+ + {value.uris && value.uris.length > 0 && ( +
+

Links

+ +
+ )} + +
+

+ Record Created: {formatDate(value.createdAt)} +

+
+
+ ); +}; + +function getFriendlyUntilDate(date: Date) { + const now = new Date(); + const diffInMs = date.getTime() - now.getTime(); + + if (diffInMs <= 0) { + return "expired"; + } + + const diffInSeconds = Math.floor(diffInMs / 1000); + const diffInMinutes = Math.floor(diffInSeconds / 60); + const diffInHours = Math.floor(diffInMinutes / 60); + const diffInDays = Math.floor(diffInHours / 24); + + if (diffInSeconds < 60) { + return `in ${diffInSeconds} second${diffInSeconds === 1 ? "" : "s"}`; + } else if (diffInMinutes < 60) { + return `in ${diffInMinutes} minute${diffInMinutes === 1 ? "" : "s"}`; + } else if (diffInHours < 24) { + const remainingMinutes = diffInMinutes % 60; + let result = `in ${diffInHours} hour${diffInHours === 1 ? "" : "s"}`; + if (remainingMinutes > 0) { + result += ` and ${remainingMinutes} minute${remainingMinutes === 1 ? "" : "s"}`; + } + return result; + } else if (diffInDays === 1) { + return "tomorrow"; + } else if (diffInDays < 7) { + const days = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]; + const weekday = days[date.getDay()]; + return `on ${weekday} (${diffInDays} days from now)`; + } else if (diffInDays < 14) { + return `next week (${diffInDays} days left)`; + } else if (diffInDays < 30) { + const weeks = Math.floor(diffInDays / 7); + const remainingDays = diffInDays % 7; + let result = `in ${weeks} week${weeks > 1 ? "s" : ""}`; + if (remainingDays > 0) { + result += ` and ${remainingDays} day${remainingDays > 1 ? "s" : ""}`; + } + return result; + } else { + const months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + return `on ${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} (${diffInDays} days from now)`; + } +} + +export default EventsSmokesignalCalendarEventView; diff --git a/src/components/views/getView.tsx b/src/components/views/getView.tsx index cbab59c..27c8bb0 100644 --- a/src/components/views/getView.tsx +++ b/src/components/views/getView.tsx @@ -8,6 +8,7 @@ import { AppBskyFeedRepostView } from "./appBsky/feedRepost"; import { AppBskyFeedLikeView } from "./appBsky/feedLike"; import { AppBskyActorProfileView } from "./appBsky/actorProfile"; import CommunityLexiconCalendarEventView from "./CommunityLexicon/calendarEvent"; +import EventsSmokesignalCalendarEventView from "./eventsSmokesignal/calendarEvent"; export type CollectionViewComponent = ( props: React.HTMLAttributes & T, @@ -28,6 +29,7 @@ const viewMap: Record< "app.bsky.feed.like": AppBskyFeedLikeView, "app.bsky.actor.profile": AppBskyActorProfileView, "community.lexicon.calendar.event": CommunityLexiconCalendarEventView, + "events.smokesignal.calendar.event": EventsSmokesignalCalendarEventView, }; const getView = (