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 && (
+
+ )}
+
+
+
+ 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 = (