Statusphere running on a slice 馃崟
1import type { ComponentChildren } from "preact";
2
3interface LayoutProps {
4 title: string;
5 currentUser: {
6 isAuthenticated: boolean;
7 handle?: string;
8 };
9 children: ComponentChildren;
10}
11
12export function Layout({ title, currentUser, children }: LayoutProps) {
13 return (
14 <html lang="en">
15 <head>
16 <meta charset="UTF-8" />
17 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
18 <title>{title}</title>
19 <script src="https://unpkg.com/htmx.org@2.0.2"></script>
20 <script dangerouslySetInnerHTML={{ __html: timezoneScript }} />
21 <style dangerouslySetInnerHTML={{ __html: styles }} />
22 </head>
23 <body>
24 <nav class="nav">
25 <div class="nav-container">
26 <a href="/" class="nav-brand">
27 Statusphere
28 </a>
29 <div class="nav-user">
30 {currentUser.isAuthenticated ? (
31 <div class="user-info">
32 <span class="handle">@{currentUser.handle}</span>
33 <form method="post" action="/logout">
34 <button type="submit" class="btn btn-secondary">
35 Logout
36 </button>
37 </form>
38 </div>
39 ) : (
40 <a href="/login" class="btn btn-primary">
41 Login
42 </a>
43 )}
44 </div>
45 </div>
46 </nav>
47 <main class="container">
48 {children}
49 </main>
50 </body>
51 </html>
52 );
53}
54
55const 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`;
72
73const styles = `
74 /* Josh's CSS Reset */
75 *, *::before, *::after { box-sizing: border-box; }
76 * { margin: 0; }
77 body { line-height: 1.5; -webkit-font-smoothing: antialiased; }
78 img, picture, video, canvas, svg { display: block; max-width: 100%; }
79 input, button, textarea, select { font: inherit; }
80 p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }
81
82 /* Custom Properties */
83 :root {
84 --primary-50: #eff6ff;
85 --primary-100: #dbeafe;
86 --primary-200: #bfdbfe;
87 --primary-300: #93c5fd;
88 --primary-400: #60a5fa;
89 --primary-500: #3b82f6;
90 --primary-600: #2563eb;
91 --primary-700: #1d4ed8;
92 --primary-800: #1e40af;
93 --primary-900: #1e3a8a;
94
95 --gray-50: #f9fafb;
96 --gray-100: #f3f4f6;
97 --gray-200: #e5e7eb;
98 --gray-300: #d1d5db;
99 --gray-400: #9ca3af;
100 --gray-500: #6b7280;
101 --gray-600: #4b5563;
102 --gray-700: #374151;
103 --gray-800: #1f2937;
104 --gray-900: #111827;
105
106 --error-500: #ef4444;
107 --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
108 }
109
110 body {
111 font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
112 background: var(--gray-50);
113 color: var(--gray-900);
114 }
115
116 .nav {
117 background: white;
118 border-bottom: 1px solid var(--gray-200);
119 padding: 1rem 0;
120 }
121
122 .nav-container {
123 max-width: 600px;
124 margin: 0 auto;
125 padding: 0 1rem;
126 display: flex;
127 justify-content: space-between;
128 align-items: center;
129 }
130
131 .nav-brand {
132 font-size: 1.25rem;
133 font-weight: 600;
134 color: var(--primary-600);
135 text-decoration: none;
136 }
137
138 .nav-user {
139 display: flex;
140 align-items: center;
141 gap: 1rem;
142 }
143
144 .user-info {
145 display: flex;
146 align-items: center;
147 gap: 1rem;
148 }
149
150 .handle {
151 color: var(--gray-600);
152 }
153
154 .container {
155 max-width: 600px;
156 margin: 0 auto;
157 padding: 2rem 1rem;
158 }
159
160 .btn {
161 padding: 0.5rem 1rem;
162 border: none;
163 border-radius: 0.375rem;
164 font-size: 0.875rem;
165 font-weight: 500;
166 cursor: pointer;
167 text-decoration: none;
168 display: inline-flex;
169 align-items: center;
170 justify-content: center;
171 transition: all 0.15s ease-in-out;
172 }
173
174 .btn-primary {
175 background: var(--primary-600);
176 color: white;
177 }
178
179 .btn-primary:hover {
180 background: var(--primary-700);
181 box-shadow: var(--shadow);
182 }
183
184 .btn-secondary {
185 background: var(--gray-100);
186 color: var(--gray-700);
187 border: 1px solid var(--gray-300);
188 }
189
190 .btn-secondary:hover {
191 background: var(--gray-200);
192 }
193
194 .card {
195 background: white;
196 border: 1px solid var(--gray-200);
197 border-radius: 0.5rem;
198 padding: 1.5rem;
199 box-shadow: var(--shadow);
200 margin-bottom: 1rem;
201 }
202
203 .form-group {
204 margin-bottom: 1rem;
205 }
206
207 .form-label {
208 display: block;
209 font-weight: 500;
210 margin-bottom: 0.25rem;
211 color: var(--gray-700);
212 }
213
214 .form-input {
215 width: 100%;
216 padding: 0.5rem 0.75rem;
217 border: 1px solid var(--gray-300);
218 border-radius: 0.375rem;
219 font-size: 0.875rem;
220 }
221
222 .form-input:focus {
223 outline: none;
224 border-color: var(--primary-500);
225 box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
226 }
227
228 .status-options {
229 display: flex;
230 flex-wrap: wrap;
231 gap: 0.5rem;
232 margin: 1rem 0;
233 }
234
235 .status-option {
236 width: 3rem;
237 height: 3rem;
238 border: 2px solid var(--gray-200);
239 border-radius: 50%;
240 background: white;
241 display: flex;
242 align-items: center;
243 justify-content: center;
244 font-size: 1.5rem;
245 cursor: pointer;
246 transition: all 0.15s ease-in-out;
247 }
248
249 .status-option:hover {
250 border-color: var(--primary-300);
251 box-shadow: var(--shadow);
252 }
253
254 .status-timeline {
255 margin-top: 2rem;
256 }
257
258 .status-item {
259 display: flex;
260 align-items: center;
261 gap: 1rem;
262 padding: 1rem 0;
263 border-bottom: 1px solid var(--gray-100);
264 }
265
266 .status-item:last-child {
267 border-bottom: none;
268 }
269
270 .status-emoji {
271 font-size: 1.5rem;
272 width: 2.5rem;
273 text-align: center;
274 }
275
276 .status-meta {
277 color: var(--gray-600);
278 font-size: 0.875rem;
279 }
280
281 .error {
282 color: var(--error-500);
283 font-size: 0.875rem;
284 margin-top: 0.25rem;
285 padding: 0.75rem;
286 background: #fef2f2;
287 border: 1px solid #fecaca;
288 border-radius: 0.375rem;
289 margin-bottom: 1rem;
290 }
291
292 .form-container {
293 margin-top: 1.5rem;
294 }
295
296 .form-help {
297 font-size: 0.75rem;
298 color: var(--gray-500);
299 margin-top: 0.25rem;
300 }
301
302 .btn-block {
303 width: 100%;
304 }
305
306 .btn-text {
307 display: inline;
308 }
309
310 .signup-prompt {
311 margin-top: 1.5rem;
312 text-align: center;
313 padding-top: 1.5rem;
314 border-top: 1px solid var(--gray-200);
315 }
316
317 .link {
318 color: var(--primary-600);
319 text-decoration: none;
320 }
321
322 .link:hover {
323 text-decoration: underline;
324 }
325
326 .htmx-indicator {
327 display: none;
328 color: var(--gray-500);
329 font-size: 0.875rem;
330 }
331
332 .htmx-request .htmx-indicator {
333 display: inline;
334 }
335
336 .htmx-request .btn-text {
337 display: none;
338 }
339`;