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

add calendar event lexicon

Natalie B f1047289 55ab46e6

Changed files
+208 -4
src
+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/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/feedPost.tsx src/components/views/appBsky/feedPost.tsx
src/components/views/app-bsky/feedRepost.tsx src/components/views/appBsky/feedRepost.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 = (