Fork of atp.tools as a universal profile for people on the ATmosphere

add smokesignal event

Natalie B aedf3316 4b737135

Changed files
+210
src
components
views
eventsSmokesignal
+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
··· 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 = (