this repo has no description
1<script lang="ts"> 2 import { onMount } from 'svelte' 3 import { _ } from '../lib/i18n' 4 import { getAuthState } from '../lib/auth.svelte' 5 import { getServerConfigState } from '../lib/serverConfig.svelte' 6 import { api } from '../lib/api' 7 8 const auth = getAuthState() 9 const serverConfig = getServerConfigState() 10 const sourceUrl = 'https://tangled.org/lewis.moe/bspds-sandbox' 11 12 let pdsHostname = $state<string | null>(null) 13 let pdsVersion = $state<string | null>(null) 14 let userCount = $state<number | null>(null) 15 16 onMount(() => { 17 api.describeServer().then(info => { 18 if (info.availableUserDomains?.length) { 19 pdsHostname = info.availableUserDomains[0] 20 } 21 if (info.version) { 22 pdsVersion = info.version 23 } 24 }).catch(() => {}) 25 26 api.listRepos(1000).then(data => { 27 userCount = data.repos.length 28 }).catch(() => {}) 29 30 const pattern = document.getElementById('dotPattern') 31 if (!pattern) return 32 33 const spacing = 32 34 const cols = Math.ceil((window.innerWidth + 600) / spacing) 35 const rows = Math.ceil((window.innerHeight + 100) / spacing) 36 const dots: { el: HTMLElement; x: number; y: number }[] = [] 37 38 for (let y = 0; y < rows; y++) { 39 for (let x = 0; x < cols; x++) { 40 const dot = document.createElement('div') 41 dot.className = 'dot' 42 dot.style.left = (x * spacing) + 'px' 43 dot.style.top = (y * spacing) + 'px' 44 pattern.appendChild(dot) 45 dots.push({ el: dot, x: x * spacing, y: y * spacing }) 46 } 47 } 48 49 let mouseX = -1000 50 let mouseY = -1000 51 52 const handleMouseMove = (e: MouseEvent) => { 53 mouseX = e.clientX 54 mouseY = e.clientY 55 } 56 57 document.addEventListener('mousemove', handleMouseMove) 58 59 let animationId: number 60 61 function updateDots() { 62 const patternRect = pattern.getBoundingClientRect() 63 dots.forEach(dot => { 64 const dotX = patternRect.left + dot.x + 5 65 const dotY = patternRect.top + dot.y + 5 66 const dist = Math.hypot(mouseX - dotX, mouseY - dotY) 67 const maxDist = 120 68 const scale = Math.min(1, Math.max(0.1, dist / maxDist)) 69 dot.el.style.transform = `scale(${scale})` 70 }) 71 animationId = requestAnimationFrame(updateDots) 72 } 73 updateDots() 74 75 return () => { 76 document.removeEventListener('mousemove', handleMouseMove) 77 cancelAnimationFrame(animationId) 78 } 79 }) 80</script> 81 82<div class="pattern-container"> 83 <div class="pattern" id="dotPattern"></div> 84</div> 85<div class="pattern-fade"></div> 86 87<nav> 88 <div class="nav-left"> 89 {#if serverConfig.hasLogo} 90 <img src="/logo" alt="Logo" class="nav-logo" /> 91 {/if} 92 {#if pdsHostname} 93 <span class="hostname">{pdsHostname}</span> 94 {#if userCount !== null} 95 <span class="user-count">{userCount} {userCount === 1 ? 'user' : 'users'}</span> 96 {/if} 97 {:else} 98 <span class="hostname placeholder">loading...</span> 99 {/if} 100 </div> 101 <span class="nav-meta">{pdsVersion || ''}</span> 102</nav> 103 104<div class="home"> 105 <section class="hero"> 106 <h1>A home for your ATProto account</h1> 107 108 <p class="lede">Tranquil PDS is a Personal Data Server, the thing that stores your posts, profile, and keys. Bluesky runs one for you, but you can run your own.</p> 109 110 <div class="actions"> 111 {#if auth.session} 112 <a href="#/dashboard" class="btn primary">@{auth.session.handle}</a> 113 {:else} 114 <a href="#/register" class="btn primary">Join This Server</a> 115 <a href={sourceUrl} class="btn secondary" target="_blank" rel="noopener">Run Your Own</a> 116 {/if} 117 </div> 118 119 <blockquote> 120 <p>"Nature does not hurry, yet everything is accomplished."</p> 121 <cite>Lao Tzu</cite> 122 </blockquote> 123 </section> 124 125 <section class="content"> 126 <h2>What you get</h2> 127 128 <div class="features"> 129 <div class="feature"> 130 <h3>Real security</h3> 131 <p>Sign in with passkeys, add two-factor authentication, set up backup codes, and mark devices you trust. Your account stays yours.</p> 132 </div> 133 134 <div class="feature"> 135 <h3>Your own identity</h3> 136 <p>Use your own domain as your handle, or get a subdomain on ours. Either way, your identity moves with you if you ever leave.</p> 137 </div> 138 139 <div class="feature"> 140 <h3>Stay in the loop</h3> 141 <p>Get important alerts where you actually see them: email, Discord, Telegram, or Signal.</p> 142 </div> 143 144 <div class="feature"> 145 <h3>You decide what apps can do</h3> 146 <p>When an app asks for access, you'll see exactly what it wants in plain language. Grant what makes sense, deny what doesn't.</p> 147 </div> 148 </div> 149 150 <h2>Everything in one place</h2> 151 152 <p>Manage your profile, security settings, connected apps, and more from a clean dashboard. No command line or 3rd party apps required.</p> 153 154 <h2>Works with everything</h2> 155 156 <p>Use any ATProto app you already like. Tranquil PDS speaks the same language as Bluesky's servers, so all your favorite clients, tools, and bots just work.</p> 157 158 <h2>Ready to try it?</h2> 159 160 <p>Join this server, or grab the source and run your own. Either way, you can migrate an existing account over and your followers, posts, and identity come with you.</p> 161 162 <div class="actions"> 163 {#if auth.session} 164 <a href="#/dashboard" class="btn primary">@{auth.session.handle}</a> 165 {:else} 166 <a href="#/register" class="btn primary">Join This Server</a> 167 <a href={sourceUrl} class="btn secondary" target="_blank" rel="noopener">View Source</a> 168 {/if} 169 </div> 170 </section> 171 172 <footer class="site-footer"> 173 <span>Open Source</span> 174 <span>Made with patience</span> 175 </footer> 176</div> 177 178<style> 179 .pattern-container { 180 position: fixed; 181 top: -32px; 182 left: -32px; 183 right: -32px; 184 bottom: -32px; 185 pointer-events: none; 186 z-index: 1; 187 overflow: hidden; 188 } 189 190 .pattern { 191 position: absolute; 192 top: 0; 193 left: 0; 194 width: calc(100% + 500px); 195 height: 100%; 196 animation: drift 80s linear infinite; 197 } 198 199 .pattern :global(.dot) { 200 position: absolute; 201 width: 10px; 202 height: 10px; 203 background: rgba(0, 0, 0, 0.06); 204 border-radius: 50%; 205 transition: transform 0.04s linear; 206 } 207 208 @media (prefers-color-scheme: dark) { 209 .pattern :global(.dot) { 210 background: rgba(255, 255, 255, 0.1); 211 } 212 } 213 214 .pattern-fade { 215 position: fixed; 216 top: 0; 217 left: 0; 218 right: 0; 219 bottom: 0; 220 background: linear-gradient(135deg, transparent 50%, var(--bg-primary) 75%); 221 pointer-events: none; 222 z-index: 2; 223 } 224 225 @keyframes drift { 226 0% { transform: translateX(-500px); } 227 100% { transform: translateX(0); } 228 } 229 230 nav { 231 position: fixed; 232 top: 12px; 233 left: 32px; 234 right: 32px; 235 background: var(--accent); 236 padding: 10px 18px; 237 z-index: 100; 238 border-radius: var(--radius-xl); 239 display: flex; 240 justify-content: space-between; 241 align-items: center; 242 } 243 244 .nav-left { 245 display: flex; 246 align-items: center; 247 gap: var(--space-3); 248 } 249 250 .nav-logo { 251 height: 28px; 252 width: auto; 253 object-fit: contain; 254 border-radius: var(--radius-sm); 255 } 256 257 .hostname { 258 font-weight: var(--font-semibold); 259 font-size: var(--text-base); 260 letter-spacing: 0.08em; 261 color: var(--text-inverse); 262 text-transform: uppercase; 263 } 264 265 .hostname.placeholder { 266 opacity: 0.4; 267 } 268 269 .user-count { 270 font-size: var(--text-sm); 271 color: rgba(255, 255, 255, 0.85); 272 padding: 4px 10px; 273 background: rgba(255, 255, 255, 0.15); 274 border-radius: var(--radius-md); 275 } 276 277 .nav-meta { 278 font-size: var(--text-sm); 279 color: rgba(255, 255, 255, 0.7); 280 letter-spacing: 0.05em; 281 } 282 283 .home { 284 position: relative; 285 z-index: 10; 286 max-width: var(--width-xl); 287 margin: 0 auto; 288 padding: 72px 32px 32px; 289 } 290 291 .hero { 292 padding: var(--space-7) 0 var(--space-8); 293 border-bottom: 1px solid var(--border-color); 294 margin-bottom: var(--space-8); 295 } 296 297 h1 { 298 font-size: var(--text-4xl); 299 font-weight: var(--font-semibold); 300 line-height: var(--leading-tight); 301 margin-bottom: var(--space-6); 302 letter-spacing: -0.02em; 303 } 304 305 .lede { 306 font-size: var(--text-xl); 307 font-weight: var(--font-medium); 308 color: var(--text-primary); 309 line-height: var(--leading-relaxed); 310 margin-bottom: 0; 311 } 312 313 .actions { 314 display: flex; 315 gap: var(--space-4); 316 margin-top: var(--space-7); 317 } 318 319 .btn { 320 font-size: var(--text-sm); 321 font-weight: var(--font-medium); 322 text-transform: uppercase; 323 letter-spacing: 0.06em; 324 padding: var(--space-4) var(--space-6); 325 border-radius: var(--radius-lg); 326 text-decoration: none; 327 transition: all var(--transition-normal); 328 border: 1px solid transparent; 329 } 330 331 .btn.primary { 332 background: var(--secondary); 333 color: var(--text-inverse); 334 border-color: var(--secondary); 335 } 336 337 .btn.primary:hover { 338 background: var(--secondary-hover); 339 border-color: var(--secondary-hover); 340 } 341 342 .btn.secondary { 343 background: transparent; 344 color: var(--text-primary); 345 border-color: var(--border-color); 346 } 347 348 .btn.secondary:hover { 349 background: var(--secondary-muted); 350 border-color: var(--secondary); 351 color: var(--secondary); 352 } 353 354 blockquote { 355 margin: var(--space-8) 0 0 0; 356 padding: var(--space-6); 357 background: var(--accent-muted); 358 border-left: 3px solid var(--accent); 359 border-radius: 0 var(--radius-xl) var(--radius-xl) 0; 360 } 361 362 blockquote p { 363 font-size: var(--text-lg); 364 color: var(--text-primary); 365 font-style: italic; 366 margin-bottom: var(--space-3); 367 } 368 369 blockquote cite { 370 font-size: var(--text-sm); 371 color: var(--text-secondary); 372 font-style: normal; 373 text-transform: uppercase; 374 letter-spacing: 0.05em; 375 } 376 377 .content h2 { 378 font-size: var(--text-sm); 379 font-weight: var(--font-bold); 380 text-transform: uppercase; 381 letter-spacing: 0.1em; 382 color: var(--accent-light); 383 margin: var(--space-8) 0 var(--space-5); 384 } 385 386 .content h2:first-child { 387 margin-top: 0; 388 } 389 390 .content > p { 391 font-size: var(--text-base); 392 color: var(--text-secondary); 393 margin-bottom: var(--space-5); 394 line-height: var(--leading-relaxed); 395 } 396 397 .features { 398 display: grid; 399 grid-template-columns: repeat(2, 1fr); 400 gap: var(--space-6); 401 margin: var(--space-6) 0 var(--space-8); 402 } 403 404 .feature { 405 padding: var(--space-5); 406 background: var(--bg-secondary); 407 border-radius: var(--radius-xl); 408 border: 1px solid var(--border-color); 409 } 410 411 .feature h3 { 412 font-size: var(--text-base); 413 font-weight: var(--font-semibold); 414 color: var(--text-primary); 415 margin-bottom: var(--space-3); 416 } 417 418 .feature p { 419 font-size: var(--text-sm); 420 color: var(--text-secondary); 421 margin: 0; 422 line-height: var(--leading-relaxed); 423 } 424 425 @media (max-width: 700px) { 426 .features { 427 grid-template-columns: 1fr; 428 } 429 430 h1 { 431 font-size: var(--text-3xl); 432 } 433 434 .actions { 435 flex-direction: column; 436 } 437 438 .btn { 439 text-align: center; 440 } 441 442 .nav-meta { 443 display: none; 444 } 445 } 446 447 .site-footer { 448 margin-top: var(--space-9); 449 padding-top: var(--space-7); 450 display: flex; 451 justify-content: space-between; 452 font-size: var(--text-sm); 453 color: var(--text-muted); 454 text-transform: uppercase; 455 letter-spacing: 0.05em; 456 border-top: 1px solid var(--border-color); 457 } 458</style>