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