johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
<script lang="ts">
import { onMount } from 'svelte';
import { LOGIN } from '#lib/login.ts';
import HandleInput from '#lib/molecules/HandleInput.svelte';
import Dialog from '#lib/molecules/Dialog.svelte';
/**
* Logging in with an Atmosphere account: the handle, then the account's
* own server to approve it on, which sends the browser back to this page.
* Picking an account the field offers logs in as it at once. One with
* no account makes one first, at the PDS the server signs people up on.
* A page sent here as `?login=<path>`, as a private site sends one not
* logged in, opens it at once, and goes back to that path after.
*/
// Apps on the Atmosphere, any of whose accounts logs in here: a row
// running past, twice over so it wraps without a seam.
const apps = [
'bluesky',
'tangled',
'leaflet',
'semble',
'skylight',
'popfeed',
'anisota',
'flashes',
'spark',
'stream-place',
'blento',
'germ',
'gander',
'offprint',
'pckt',
'plyr',
'beacon-bits',
'eurosky',
'northsky',
'blacksky'
];
const icon = (app: string) => `/atmo/${app}.${app === 'spark' ? 'png' : 'jpg'}`;
let handle = $state('');
let problem = $state('');
// Which is under way, until the page goes to the PDS: 'login' or 'create'.
let busy = $state('');
// Where to go back to once logged in, if the address names a path here.
function asked(): string | null {
const back = new URLSearchParams(location.search).get('login');
return back?.startsWith('/') && !back.startsWith('//') ? back : null;
}
onMount(() => {
if (asked()) document.getElementById(LOGIN)?.showPopover();
});
// A page linked from the dialog is read without it over the page.
function close() {
document.getElementById(LOGIN)?.hidePopover();
}
function submit(e: SubmitEvent) {
e.preventDefault();
login();
}
async function login(create = false) {
if (busy) return;
busy = create ? 'create' : 'login';
problem = '';
const back = asked() ?? `${location.pathname}${location.search}`;
const res = await fetch('/oauth/login', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(create ? { create, back } : { handle, back })
}).catch(() => null);
// The page to approve it on, or why not, in words.
const text = (await res?.text().catch(() => '')) ?? '';
if (res?.ok) {
location.assign(JSON.parse(text).url);
return;
}
problem = (res && res.status < 500 && text) || 'Logging in did not work. Try again.';
busy = '';
}
</script>
<Dialog id={LOGIN} title="Log in with your Atmosphere account">
<div class="apps" aria-hidden="true">
<div class="apps-run">
{#each [...apps, ...apps] as app, i (i)}<img
src={icon(app)}
alt=""
width="40"
height="40"
loading="lazy"
/>{/each}
</div>
</div>
<p class="what">
<a href="https://atmosphereaccount.com/" target="_blank" rel="noopener noreferrer"
>What is an Atmosphere account?</a
>
</p>
<form onsubmit={submit}>
<label for="login-handle">Handle</label>
<HandleInput id="login-handle" bind:value={handle} onpick={() => login()} />
{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
<p class="agree">
By logging in you agree to the <a href="/terms" onclick={close}>terms</a>,
<a href="/privacy" onclick={close}>privacy policy</a>
and <a href="/guidelines" onclick={close}>community guidelines</a>.
</p>
<div class="actions">
<button class="btn" type="button" popovertarget={LOGIN} popovertargetaction="hide"
>Cancel</button
>
<button class="btn primary" type="submit" disabled={!!busy} aria-busy={busy === 'login'}
>{busy === 'login' ? 'Logging in…' : 'Log in'}</button
>
</div>
</form>
<p class="create">
Don't have an account? <button type="button" onclick={() => login(true)} disabled={!!busy}
>{busy === 'create' ? 'Opening…' : 'Create one'}</button
>
</p>
</Dialog>