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 and tools 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>Made by people who don't take themselves too seriously</span>
174 <span>Open Source: issues & PRs welcome</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>