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

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
168 lines · 5.8 KB Raw History
<script lang="ts">
	import type * as getOwnProfile from '#lib/lexicons/dev/knotless/getOwnProfile.ts';
	import type * as getProfile from '#lib/lexicons/dev/knotless/getProfile.ts';
	import type * as updateProfile from '#lib/lexicons/dev/knotless/updateProfile.ts';
	import type * as uploadAvatar from '#lib/lexicons/dev/knotless/uploadAvatar.ts';
	import type { UriString } from '@atproto/lex';
	import { untrack } from 'svelte';
	import { goto } from '$app/navigation';
	import { call, upload } from '#lib/data.ts';
	import { urlPath } from '#lib/format.ts';
	import Avatar from '#lib/atoms/Avatar.svelte';
	import LinkRows from '#lib/molecules/LinkRows.svelte';
	/**
	 * Changing how the account shows on knotless: its knotless profile,
	 * `own`, each field of which stands in for its Bluesky profile's, as
	 * `shown` has the two together. Saved, the profile page shows it.
	 */
	let { own, shown }: { own: getOwnProfile.OwnProfile; shown: getProfile.ProfileView } = $props();
	/** The most links a profile takes, and the most bytes of a picture sent to be made to fit. */
	const LINKS = 10;
	const PICTURE = 30_000_000;
	const handle = $derived(shown.handle ?? shown.did);
	// What the Bluesky profile shows for a field the knotless one leaves out;
	// for one it sets, unknown here.
	const bluesky = $derived({
		name: own.displayName ? undefined : shown.displayName,
		bio: own.description ? undefined : shown.description,
		pronouns: own.pronouns ? undefined : shown.pronouns,
		links: own.links?.length ? [] : (shown.links ?? []),
		avatar: own.avatar ? undefined : shown.avatar
	});
	// The form starts from the profile as it is, and is the person's after.
	let name = $state(untrack(() => own.displayName ?? ''));
	let bio = $state(untrack(() => own.description ?? ''));
	let pronouns = $state(untrack(() => own.pronouns ?? ''));
	let links = $state(
		untrack(() => (own.links ?? []).map((l) => ({ url: l.url as string, title: l.title ?? '' })))
	);
	let bot = $state(untrack(() => !!own.bot));
	let file = $state<File | null>(null);
	let removed = $state(false);
	let busy = $state(false);
	let problem = $state('');
	let picked = $state<string | null>(null);
	$effect(() => {
		if (!file) return;
		const url = URL.createObjectURL(file);
		picked = url;
		return () => URL.revokeObjectURL(url);
	});
	const avatar = $derived(
		file ? picked : removed ? bluesky.avatar : (own.avatar ?? bluesky.avatar)
	);
	const filled = $derived(links.filter((l) => l.url.trim()));
	function choose(e: Event) {
		const chosen = (e.currentTarget as HTMLInputElement).files?.[0] ?? null;
		problem = '';
		if (chosen && !['image/png', 'image/jpeg', 'image/webp', 'image/gif'].includes(chosen.type))
			problem = 'A picture is a PNG, JPEG, WebP or GIF.';
		else if (chosen && chosen.size > PICTURE)
			problem = `That picture is larger than ${PICTURE.toLocaleString('en')} bytes.`;
		else {
			file = chosen;
			removed = false;
		}
	}
	function unpick() {
		file = null;
		removed = !!own.avatar;
	}
	async function save(e: SubmitEvent) {
		e.preventDefault();
		busy = true;
		problem = '';
		try {
			if (file) await upload<typeof uploadAvatar.main>('dev.knotless.uploadAvatar', file);
			const titled = filled.map((l) => ({
				url: l.url.trim() as UriString,
				title: l.title.trim() || undefined
			}));
			await call<typeof updateProfile.main>('dev.knotless.updateProfile', {
				displayName: name,
				description: bio,
				pronouns,
				links: titled,
				removeAvatar: removed && !file ? true : undefined,
				bot
			});
			await goto(`/${urlPath(handle)}`, { invalidateAll: true });
		} catch (err) {
			problem = (err as Error).message;
		} finally {
			busy = false;
		}
	}
</script>

<div class="settings-section"><h2>Public profile</h2></div>
<div class="profile-form">
	<form class="settings-form" onsubmit={save}>
		<p class="mute">
			Each field stands in for your Bluesky profile's: one left empty shows Bluesky's.
		</p>
		<div class="field">
			<span class="field-label">Profile picture</span>
			<div class="picture">
				<Avatar name={handle} did={shown.did} size="xl" src={avatar} />
				<div class="picture-actions">
					<label class="btn"
						>Upload a picture<input
							class="file"
							type="file"
							accept="image/png,image/jpeg,image/webp,image/gif"
							onchange={choose}
						/></label
					>
					{#if file || (own.avatar && !removed)}<button class="btn" type="button" onclick={unpick}
							>{file ? 'Keep the one there is' : 'Remove'}</button
						>{/if}
					<p class="hint">A PNG, JPEG, WebP or GIF. A large one is made smaller.</p>
				</div>
			</div>
		</div>
		<label class="field"
			><span class="field-label">Name</span><input
				name="name"
				maxlength="64"
				placeholder={bluesky.name ?? ''}
				bind:value={name}
			/></label
		>
		<label class="field"
			><span class="field-label">Bio</span><textarea
				name="bio"
				rows="4"
				maxlength="256"
				placeholder={bluesky.bio ?? 'Tell us a little about yourself'}
				bind:value={bio}></textarea></label
		>
		<label class="field"
			><span class="field-label">Pronouns</span><input
				name="pronouns"
				maxlength="20"
				placeholder={bluesky.pronouns ?? ''}
				bind:value={pronouns}
			/></label
		>
		<fieldset class="field">
			<legend class="field-label">Links</legend>
			<LinkRows bind:links max={LINKS} />
			{#if !links.length && bluesky.links.length}<p class="hint">
					Shows your Bluesky profile's links until you add one.
				</p>{/if}
		</fieldset>
		<div class="field">
			<label class="check"
				><input type="checkbox" name="bot" bind:checked={bot} />This account is a bot</label
			>
			<p class="hint">What it says and pushes stays off the front page.</p>
		</div>
		{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
		<div>
			<button class="btn primary" type="submit" disabled={busy} aria-busy={busy}
				>{busy ? 'Saving…' : 'Update profile'}</button
			>
		</div>
	</form>
</div>