+3
-2
src/components/App.tsx
+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
+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
+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
+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
+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
+
}