<script lang="ts">
import { loggingIn, type Account } from '$lib/accounts';
import { AtpClient, resolveHandle } from '$lib/at/client.svelte';
import type { Handle } from '@atcute/lexicons';
import ProfilePicture from './ProfilePicture.svelte';
import Popup from './Popup.svelte';
import Dropdown from './Dropdown.svelte';
import AccountSelector from './AccountSelector.svelte';
import { flow } from '$lib/at/oauth';
import { isHandle, type AtprotoDid } from '@atcute/lexicons/syntax';
import Icon from '@iconify/svelte';
interface Props {
client: AtpClient;
accounts: Array<Account>;
selectedDid?: AtprotoDid | null;
onAccountSelected: (did: AtprotoDid) => void;
onLogout: (did: AtprotoDid) => void;
}
let {
client,
accounts = [],
selectedDid = $bindable(null),
onAccountSelected,
onLogout
}: Props = $props();
let isDropdownOpen = $state(false);
let isLoginModalOpen = $state(false);
let loginHandle = $state('');
let loginError = $state('');
let isLoggingIn = $state(false);
const toggleDropdown = () => (isDropdownOpen = !isDropdownOpen);
const closeDropdown = () => (isDropdownOpen = false);
const selectAccount = (did: AtprotoDid) => {
onAccountSelected(did);
closeDropdown();
};
const openLoginModal = () => {
isLoginModalOpen = true;
closeDropdown();
loginHandle = '';
loginError = '';
// HACK: i hate this but it works so it doesnt really matter
setTimeout(() => document.getElementById('handle')?.focus(), 100);
};
const closeLoginModal = () => {
document.getElementById('handle')?.blur();
isLoginModalOpen = false;
loginHandle = '';
loginError = '';
};
const handleLogin = async () => {
try {
if (!loginHandle) throw 'please enter handle';
isLoggingIn = true;
loginError = '';
let handle: Handle;
if (isHandle(loginHandle)) handle = loginHandle;
else throw 'handle is invalid';
let did = await resolveHandle(handle);
if (!did.ok) throw did.error;
await initiateLogin(did.value, handle);
} catch (error) {
loginError = `login failed: ${error}`;
loggingIn.set(null);
} finally {
isLoggingIn = false;
}
};
const initiateLogin = async (did: AtprotoDid, handle: Handle | null) => {
loggingIn.set({ did, handle });
const result = await flow.start(handle ?? did);
if (!result.ok) throw result.error;
};
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !isLoggingIn) handleLogin();
};
</script>
{#snippet action(name: string, icon: string, onClick: () => void)}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
title={name}
onclick={onClick}
class="hidden text-(--nucleus-accent) transition-all group-hover:block hover:scale-[1.2] hover:shadow-md"
>
<Icon class="h-5 w-5" {icon} />
</div>
{/snippet}
<Dropdown
class="min-w-52 rounded-sm border-2 border-(--nucleus-accent) bg-(--nucleus-bg) shadow-2xl"
bind:isOpen={isDropdownOpen}
placement="top-start"
>
{#snippet trigger()}
<button
onclick={toggleDropdown}
class="flex h-13 w-13 items-center justify-center rounded-sm shadow-md transition-all hover:scale-110 hover:shadow-xl hover:saturate-150"
>
<ProfilePicture {client} did={selectedDid} size={13} />
</button>
{/snippet}
<AccountSelector {accounts} {selectedDid} onSelect={selectAccount}>
{#snippet accountActions(account)}
{@render action('relogin', 'heroicons:arrow-path-rounded-square-solid', () =>
initiateLogin(account.did, account.handle)
)}
{@render action('logout', 'heroicons:trash-solid', () => onLogout(account.did))}
{/snippet}
</AccountSelector>
{#if accounts.length > 0}
<div class="mx-2 h-px bg-linear-to-r from-(--nucleus-accent) to-(--nucleus-accent2)"></div>
{/if}
<button
onclick={openLoginModal}
class="group flex w-full origin-left items-center gap-3 p-3 text-left text-sm font-semibold text-(--nucleus-accent) transition-all hover:scale-[1.1]"
>
<Icon class="h-5 w-5 scale-[130%]" icon="heroicons:plus-16-solid" />
<span>add account</span>
</button>
</Dropdown>
<Popup bind:isOpen={isLoginModalOpen} onClose={closeLoginModal} title="add account">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="space-y-2" onkeydown={handleKeydown}>
<div>
<label for="handle" class="mb-2 block text-sm font-semibold text-(--nucleus-fg)/80">
account handle
</label>
<input
id="handle"
type="text"
bind:value={loginHandle}
placeholder="example.bsky.social"
class="single-line-input border-(--nucleus-accent)/40 bg-(--nucleus-accent)/3"
disabled={isLoggingIn}
/>
</div>
{#if loginError}
<div class="error-disclaimer">
<p>
<Icon class="inline h-10 w-10" icon="heroicons:exclamation-triangle-16-solid" />
{loginError}
</p>
</div>
{/if}
<div class="flex gap-3 pt-3">
<button onclick={closeLoginModal} class="flex-1 action-button" disabled={isLoggingIn}>
cancel
</button>
<button
onclick={handleLogin}
class="flex-1 action-button border-transparent text-(--nucleus-fg)"
style="background: linear-gradient(135deg, var(--nucleus-accent), var(--nucleus-accent2));"
disabled={isLoggingIn}
>
{isLoggingIn ? 'logging in...' : 'login'}
</button>
</div>
</div>
</Popup>