Statusphere running on a slice 馃崟
at main 339 lines 7.3 kB view raw
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`;