Statusphere running on a slice ๐Ÿ•

๐Ÿ•

Changed files
+52 -10
src
+3 -2
src/components/App.tsx
··· 12 12 statuses?: HydratedStatus[]; 13 13 page?: string; 14 14 error?: string; 15 + userTimezone?: string; 15 16 } 16 17 17 - export function App({ currentUser, statuses = [], page, error }: AppProps) { 18 + export function App({ currentUser, statuses = [], page, error, userTimezone }: AppProps) { 18 19 if (page === "login") { 19 20 return ( 20 21 <Layout title="Login - Statusphere" currentUser={currentUser}> ··· 25 26 26 27 return ( 27 28 <Layout title="Statusphere" currentUser={currentUser}> 28 - <HomePage currentUser={currentUser} statuses={statuses} /> 29 + <HomePage currentUser={currentUser} statuses={statuses} userTimezone={userTimezone} /> 29 30 </Layout> 30 31 ); 31 32 }
+19 -4
src/components/HomePage.tsx
··· 6 6 handle?: string; 7 7 }; 8 8 statuses: HydratedStatus[]; 9 + userTimezone?: string; 9 10 } 10 11 11 12 const statusOptions = [ ··· 46 47 "๐Ÿ’ก", 47 48 ]; 48 49 49 - export function HomePage({ currentUser, statuses }: HomePageProps) { 50 + export function HomePage({ currentUser, statuses, userTimezone }: HomePageProps) { 50 51 return ( 51 52 <div> 52 53 <div class="card"> ··· 75 76 76 77 <div class="card"> 77 78 <h3>Recent Statuses</h3> 78 - <StatusTimeline statuses={statuses} /> 79 + <StatusTimeline statuses={statuses} userTimezone={userTimezone} /> 79 80 </div> 80 81 </div> 81 82 ); ··· 83 84 84 85 interface StatusTimelineProps { 85 86 statuses: HydratedStatus[]; 87 + userTimezone?: string; 86 88 } 87 89 88 - export function StatusTimeline({ statuses }: StatusTimelineProps) { 90 + export function StatusTimeline({ statuses, userTimezone }: StatusTimelineProps) { 89 91 return ( 90 92 <div id="status-timeline"> 91 93 {statuses.length === 0 ? ( ··· 98 100 const authorHandle = status.author?.handle || 99 101 status.did?.split(":").pop() || 100 102 "unknown"; 101 - const createdAt = new Date(status.value.createdAt).toLocaleString(); 103 + const createdAtDate = new Date(status.value.createdAt); 104 + const formatOptions: Intl.DateTimeFormatOptions = { 105 + year: 'numeric', 106 + month: 'short', 107 + day: 'numeric', 108 + hour: 'numeric', 109 + minute: '2-digit', 110 + hour12: true 111 + }; 112 + // Use user's timezone if available, otherwise let browser decide 113 + if (userTimezone) { 114 + formatOptions.timeZone = userTimezone; 115 + } 116 + const createdAt = new Intl.DateTimeFormat('en-US', formatOptions).format(createdAtDate); 102 117 103 118 return ( 104 119 <div key={status.uri} class="status-item">
+19
src/components/Layout.tsx
··· 17 17 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 18 18 <title>{title}</title> 19 19 <script src="https://unpkg.com/htmx.org@2.0.2"></script> 20 + <script dangerouslySetInnerHTML={{ __html: timezoneScript }} /> 20 21 <style dangerouslySetInnerHTML={{ __html: styles }} /> 21 22 </head> 22 23 <body> ··· 50 51 </html> 51 52 ); 52 53 } 54 + 55 + const timezoneScript = ` 56 + // Store user's timezone in a cookie for SSR 57 + (function() { 58 + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; 59 + const existingTz = document.cookie.split('; ').find(row => row.startsWith('timezone=')); 60 + const currentTzValue = existingTz ? decodeURIComponent(existingTz.split('=')[1]) : null; 61 + 62 + if (!existingTz) { 63 + // No timezone cookie exists, set it 64 + document.cookie = 'timezone=' + encodeURIComponent(tz) + '; path=/; max-age=31536000; SameSite=Lax'; 65 + } else if (currentTzValue !== tz) { 66 + // Timezone changed, update cookie and reload once 67 + document.cookie = 'timezone=' + encodeURIComponent(tz) + '; path=/; max-age=31536000; SameSite=Lax'; 68 + window.location.reload(); 69 + } 70 + })(); 71 + `; 53 72 54 73 const styles = ` 55 74 /* Josh's CSS Reset */
+6 -4
src/main.ts
··· 4 4 import { PORT, sessionStore, oauthSessions, atprotoClient } from "./config.ts"; 5 5 import { AuthenticatedUser } from "./types.ts"; 6 6 import { fetchStatusesWithAuthors } from "./api.ts"; 7 + import { getUserTimezone } from "./utils.ts"; 7 8 8 9 async function handler(req: Request): Promise<Response> { 9 10 const url = new URL(req.url); ··· 41 42 } 42 43 43 44 async function handleHome( 44 - _req: Request, 45 + req: Request, 45 46 currentUser: AuthenticatedUser 46 47 ): Promise<Response> { 47 48 const statuses = await fetchStatusesWithAuthors(); 48 - 49 - const html = render(App({ currentUser, statuses })); 49 + const userTimezone = getUserTimezone(req); 50 + const html = render(App({ currentUser, statuses, userTimezone })); 50 51 51 52 return new Response(`<!DOCTYPE html>${html}`, { 52 53 headers: { "Content-Type": "text/html" }, ··· 159 160 // Return updated timeline for HTMX 160 161 try { 161 162 const statuses = await fetchStatusesWithAuthors(); 162 - const html = render(StatusTimeline({ statuses })); 163 + const userTimezone = getUserTimezone(req); 164 + const html = render(StatusTimeline({ statuses, userTimezone })); 163 165 164 166 return new Response(html, { 165 167 headers: { "Content-Type": "text/html" },
+5
src/utils.ts
··· 1 + export function getUserTimezone(req: Request): string | undefined { 2 + const cookieHeader = req.headers.get("cookie") || ""; 3 + const timezoneCookie = cookieHeader.split("; ").find(row => row.startsWith("timezone=")); 4 + return timezoneCookie ? decodeURIComponent(timezoneCookie.split("=")[1]) : undefined; 5 + }