~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
120 lines · 3.8 KB Raw History
<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>