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