+208
src/components/views/eventsSmokesignal/calendarEvent.tsx
+208
src/components/views/eventsSmokesignal/calendarEvent.tsx
···
1
+
import { CollectionViewComponent, CollectionViewProps } from "../getView";
2
+
3
+
interface CalendarEventLexiconValue {
4
+
mode: string;
5
+
name: string;
6
+
uris: Array<{
7
+
uri: string;
8
+
name: string;
9
+
$type: string;
10
+
}>;
11
+
$type: string;
12
+
status: string;
13
+
startsAt: string;
14
+
createdAt: string;
15
+
description?: string;
16
+
text?: string;
17
+
location?: {
18
+
name: string;
19
+
street: string;
20
+
locality: string;
21
+
region: string;
22
+
};
23
+
}
24
+
25
+
interface CalendarEventLexicon {
26
+
uri: string;
27
+
cid: string;
28
+
value: CalendarEventLexiconValue;
29
+
}
30
+
31
+
const EventsSmokesignalCalendarEventView: CollectionViewComponent<
32
+
CollectionViewProps
33
+
> = ({ data }: CollectionViewProps) => {
34
+
const { value } = data as CalendarEventLexicon;
35
+
36
+
// Helper function to format date strings
37
+
const formatDate = (dateString: string): string => {
38
+
try {
39
+
const date = new Date(dateString);
40
+
const friendly = getFriendlyUntilDate(date);
41
+
return (
42
+
date.toLocaleString("en-US", {
43
+
year: "numeric",
44
+
month: "long",
45
+
day: "numeric",
46
+
hour: "numeric",
47
+
minute: "2-digit",
48
+
timeZoneName: "short",
49
+
}) + (friendly !== "expired" ? " - " + friendly : "")
50
+
);
51
+
} catch (e) {
52
+
return "Invalid Date";
53
+
}
54
+
};
55
+
56
+
// Helper function to extract the human-friendly part of status/mode
57
+
const getFriendlyTerm = (term: string): string => {
58
+
const parts = term.split("#");
59
+
return parts.length > 1
60
+
? parts[1].charAt(0).toUpperCase() + parts[1].slice(1)
61
+
: term;
62
+
};
63
+
64
+
const statusColor = (status: string): string => {
65
+
if (status.includes("scheduled")) return "green";
66
+
if (status.includes("cancelled")) return "red";
67
+
if (status.includes("postponed")) return "orange";
68
+
return "#555"; // Default color
69
+
};
70
+
71
+
return (
72
+
<div className="border p-6 py-3 rounded-md">
73
+
<h2 className="text-2xl font-semibold mb-2 w-max">📅 {value.name}</h2>
74
+
75
+
<div>
76
+
<p>
77
+
<strong>Starts:</strong> {formatDate(value.startsAt)}
78
+
</p>
79
+
<p>
80
+
<strong>Status:</strong>
81
+
<span
82
+
className="px-1 mx-1 rounded-md border border-border"
83
+
style={{ backgroundColor: statusColor(value.status) }}
84
+
>
85
+
{getFriendlyTerm(value.status)}
86
+
</span>
87
+
</p>
88
+
<p>
89
+
<strong>Where:</strong> {getFriendlyTerm(value.mode)}{" "}
90
+
{value.location &&
91
+
`${value.location.name}: ${value.location.street}, ${value.location.locality}, ${value.location.region}`}
92
+
</p>
93
+
</div>
94
+
95
+
<div>
96
+
<h3 className="text-xl my-1 font-semibold">Description</h3>
97
+
<p className="p-2 rounded-md border border-border">
98
+
{(value.description || value.text)
99
+
?.split("\n")
100
+
.map((t) => (t == "\n" ? <p></p> : <p className="mb-2">{t}</p>))}
101
+
</p>
102
+
</div>
103
+
104
+
{value.uris && value.uris.length > 0 && (
105
+
<div style={{ marginBottom: "20px" }}>
106
+
<h3 className="text-xl pt-2 font-semibold">Links</h3>
107
+
<ul style={{ listStyleType: "none", paddingLeft: 0 }}>
108
+
{value.uris.map((link, index) => (
109
+
<li key={index} style={{ marginBottom: "5px" }}>
110
+
<a
111
+
href={link.uri}
112
+
target="_blank"
113
+
rel="noopener noreferrer"
114
+
className="text-blue-700 dark:text-blue-400"
115
+
>
116
+
{link.name || link.uri}
117
+
</a>
118
+
</li>
119
+
))}
120
+
</ul>
121
+
</div>
122
+
)}
123
+
124
+
<div
125
+
style={{
126
+
fontSize: "0.8em",
127
+
color: "#7f8c8d",
128
+
borderTop: "1px solid #ecf0f1",
129
+
paddingTop: "10px",
130
+
marginTop: "20px",
131
+
}}
132
+
>
133
+
<p style={{ margin: "5px 0" }}>
134
+
<em>Record Created: {formatDate(value.createdAt)}</em>
135
+
</p>
136
+
</div>
137
+
</div>
138
+
);
139
+
};
140
+
141
+
function getFriendlyUntilDate(date: Date) {
142
+
const now = new Date();
143
+
const diffInMs = date.getTime() - now.getTime();
144
+
145
+
if (diffInMs <= 0) {
146
+
return "expired";
147
+
}
148
+
149
+
const diffInSeconds = Math.floor(diffInMs / 1000);
150
+
const diffInMinutes = Math.floor(diffInSeconds / 60);
151
+
const diffInHours = Math.floor(diffInMinutes / 60);
152
+
const diffInDays = Math.floor(diffInHours / 24);
153
+
154
+
if (diffInSeconds < 60) {
155
+
return `in ${diffInSeconds} second${diffInSeconds === 1 ? "" : "s"}`;
156
+
} else if (diffInMinutes < 60) {
157
+
return `in ${diffInMinutes} minute${diffInMinutes === 1 ? "" : "s"}`;
158
+
} else if (diffInHours < 24) {
159
+
const remainingMinutes = diffInMinutes % 60;
160
+
let result = `in ${diffInHours} hour${diffInHours === 1 ? "" : "s"}`;
161
+
if (remainingMinutes > 0) {
162
+
result += ` and ${remainingMinutes} minute${remainingMinutes === 1 ? "" : "s"}`;
163
+
}
164
+
return result;
165
+
} else if (diffInDays === 1) {
166
+
return "tomorrow";
167
+
} else if (diffInDays < 7) {
168
+
const days = [
169
+
"Sunday",
170
+
"Monday",
171
+
"Tuesday",
172
+
"Wednesday",
173
+
"Thursday",
174
+
"Friday",
175
+
"Saturday",
176
+
];
177
+
const weekday = days[date.getDay()];
178
+
return `on ${weekday} (${diffInDays} days from now)`;
179
+
} else if (diffInDays < 14) {
180
+
return `next week (${diffInDays} days left)`;
181
+
} else if (diffInDays < 30) {
182
+
const weeks = Math.floor(diffInDays / 7);
183
+
const remainingDays = diffInDays % 7;
184
+
let result = `in ${weeks} week${weeks > 1 ? "s" : ""}`;
185
+
if (remainingDays > 0) {
186
+
result += ` and ${remainingDays} day${remainingDays > 1 ? "s" : ""}`;
187
+
}
188
+
return result;
189
+
} else {
190
+
const months = [
191
+
"January",
192
+
"February",
193
+
"March",
194
+
"April",
195
+
"May",
196
+
"June",
197
+
"July",
198
+
"August",
199
+
"September",
200
+
"October",
201
+
"November",
202
+
"December",
203
+
];
204
+
return `on ${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} (${diffInDays} days from now)`;
205
+
}
206
+
}
207
+
208
+
export default EventsSmokesignalCalendarEventView;
+2
src/components/views/getView.tsx
+2
src/components/views/getView.tsx
···
8
8
import { AppBskyFeedLikeView } from "./appBsky/feedLike";
9
9
import { AppBskyActorProfileView } from "./appBsky/actorProfile";
10
10
import CommunityLexiconCalendarEventView from "./CommunityLexicon/calendarEvent";
11
+
import EventsSmokesignalCalendarEventView from "./eventsSmokesignal/calendarEvent";
11
12
12
13
export type CollectionViewComponent<T = {}> = (
13
14
props: React.HTMLAttributes<HTMLDivElement> & T,
···
28
29
"app.bsky.feed.like": AppBskyFeedLikeView,
29
30
"app.bsky.actor.profile": AppBskyActorProfileView,
30
31
"community.lexicon.calendar.event": CommunityLexiconCalendarEventView,
32
+
"events.smokesignal.calendar.event": EventsSmokesignalCalendarEventView,
31
33
};
32
34
33
35
const getView = (