feat: create account ui

This commit is contained in:
Shibo Lyu 2024-10-16 02:22:31 +08:00
parent e3e3481739
commit 2b47eeb146
14 changed files with 303 additions and 17 deletions

View file

@ -20,7 +20,7 @@
{href}
class={tw(
'inline-flex cursor-default items-center justify-center rounded-md px-2 py-1 text-sf-secondary shadow-sm ring-1 ring-ss-secondary',
'transition-shadow duration-200 hover:ring-ss-primary active:shadow-inner',
'font-normal transition-shadow duration-200 hover:ring-ss-primary active:shadow-inner',
variant === 'primary' && [
'relative text-slate-50 ring-0 duration-200',
'before:absolute before:-inset-px before:rounded-[7px]',

View file

@ -1,4 +1,6 @@
import GroupedListItem from './GroupedList/GroupedListItem.svelte';
import GroupedListInputItem from './GroupedList/GroupedListInputItem.svelte';
import GroupedListSection from './GroupedList/GroupedListSection.svelte';
import GroupedListContainer from './GroupedList/GroupedListContainer.svelte';
export { GroupedListItem, GroupedListSection };
export { GroupedListItem, GroupedListInputItem, GroupedListSection, GroupedListContainer };

View file

@ -0,0 +1 @@
<div class="mx-auto max-w-3xl"><slot /></div>

View file

@ -0,0 +1,5 @@
<label
class="flex gap-2 px-4 py-3 font-medium text-sf-primary [align-items:first_baseline] [&>input]:flex-1 [&>input]:bg-transparent [&>input]:text-end [&>input]:outline-none [&>input]:placeholder:opacity-50"
>
<slot />
</label>

View file

@ -11,7 +11,7 @@
this={href ? 'a' : 'div'}
{href}
class={tw(
'flex items-center gap-2 px-4 py-3 font-medium text-sf-primary first:rounded-t-lg last:rounded-b-lg',
'flex cursor-default items-center gap-2 px-4 py-3 font-medium text-sf-primary first:rounded-t-lg last:rounded-b-lg',
selected && 'bg-accent-500 text-white shadow-inner dark:bg-accent-900 dark:text-sf-primary'
)}
>

View file

@ -1,16 +1,16 @@
<section class="my-4 px-4">
<section class="my-6 cursor-default px-4">
{#if $$slots.header}
<h3 class="mb-1 truncate text-sm font-medium uppercase text-sf-secondary">
<h3 class="mb-1 truncate px-4 text-sm font-medium uppercase text-sf-tertiary">
<slot name="header" />
</h3>
{/if}
<div
class="divide-y-[0.5px] divide-ss-secondary rounded-lg border-[0.5px] border-ss-secondary bg-sb-primary shadow-sm"
class="divide-y-[0.5px] divide-ss-secondary overflow-hidden rounded-lg border-[0.5px] border-ss-secondary bg-sb-primary shadow-sm"
>
<slot />
</div>
{#if $$slots.footer}
<div class="mt-1 text-sm font-medium uppercase text-sf-secondary">
<div class="mt-1 px-4 text-sm text-sf-tertiary">
<slot name="footer" />
</div>
{/if}

View file

@ -0,0 +1,95 @@
<script lang="ts">
import { tw } from '$lib/tw';
let className = '';
export { className as class };
</script>
<div class={tw('loading-indicator relative inline-block size-5 [&>div]:bg-sf-tertiary', className)}>
<div class="bar1"></div>
<div class="bar2"></div>
<div class="bar3"></div>
<div class="bar4"></div>
<div class="bar5"></div>
<div class="bar6"></div>
<div class="bar7"></div>
<div class="bar8"></div>
<div class="bar9"></div>
<div class="bar10"></div>
<div class="bar11"></div>
<div class="bar12"></div>
</div>
<style>
.loading-indicator > div {
width: 6%;
height: 16%;
position: absolute;
left: 49%;
top: 43%;
opacity: 0;
border-radius: 50px;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.2);
animation: fade 1s linear infinite;
}
@keyframes fade {
from {
opacity: 1;
}
to {
opacity: 0.25;
}
}
.bar1 {
transform: rotate(0deg) translate(0, -130%);
animation-delay: 0s;
}
.bar2 {
transform: rotate(30deg) translate(0, -130%);
animation-delay: -0.9167s;
}
.bar3 {
transform: rotate(60deg) translate(0, -130%);
animation-delay: -0.833s;
}
.bar4 {
transform: rotate(90deg) translate(0, -130%);
animation-delay: -0.7497s;
}
.bar5 {
transform: rotate(120deg) translate(0, -130%);
animation-delay: -0.667s;
}
.bar6 {
transform: rotate(150deg) translate(0, -130%);
animation-delay: -0.5837s;
}
.bar7 {
transform: rotate(180deg) translate(0, -130%);
animation-delay: -0.5s;
}
.bar8 {
transform: rotate(210deg) translate(0, -130%);
animation-delay: -0.4167s;
}
.bar9 {
transform: rotate(240deg) translate(0, -130%);
animation-delay: -0.333s;
}
.bar10 {
transform: rotate(270deg) translate(0, -130%);
animation-delay: -0.2497s;
}
.bar11 {
transform: rotate(300deg) translate(0, -130%);
animation-delay: -0.167s;
}
.bar12 {
transform: rotate(330deg) translate(0, -130%);
animation-delay: -0.0833s;
}
</style>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import { tw } from '$lib/tw';
let className = '';
export { className as class };
</script>
<header
class={tw(
'flex min-h-[calc(3rem+1px)] cursor-default items-center border-b border-ss-secondary bg-sb-primary p-2 text-center font-semibold text-sf-primary shadow-sm',
className
)}
>
<slot />
</header>

View file

@ -25,7 +25,7 @@ class IdentityFileDB {
const db = await openDB<IdentityFileDBSchema>(IDB_NAME, 1, {
upgrade(db) {
if (!db.objectStoreNames.contains(IDB_OBJECT_STORE_NAME)) {
const store = db.createObjectStore(IDB_OBJECT_STORE_NAME, { keyPath: 'idKeyId' });
const store = db.createObjectStore(IDB_OBJECT_STORE_NAME, { keyPath: 'id_key' });
store.createIndex('id_urls', 'profile.signee.payload.id_urls', {
multiEntry: true,
unique: true

View file

@ -5,7 +5,8 @@
import SettingsList from './settings/SettingsList.svelte';
onNavigate((navigation) => {
if (!document.startViewTransition) return;
if (!document.startViewTransition || navigation.from?.url.href === navigation.to?.url.href)
return;
return new Promise((resolve) => {
document.startViewTransition(async () => {
@ -15,7 +16,7 @@
});
});
$: isSettings = $page.route.id?.startsWith('/(app)/settings');
$: isSettings = $page.route.id?.startsWith('/(app)/settings') ?? true;
$: mainVisible =
!!$page.params.chatId ||
(isSettings && !$page.route.id?.startsWith('/(app)/settings/_mobile_empty'));
@ -30,7 +31,7 @@
>
<ChatList />
{#if isSettings}
<SettingsList class="absolute inset-0 z-10 size-full [view-transition-name:settings-list]" />
<SettingsList class="absolute inset-0 z-10 size-full origin-top-left" />
{/if}
</aside>
{#if mainVisible}

View file

@ -3,6 +3,7 @@
import { GroupedListSection, GroupedListItem } from '$lib/components/GroupedList';
import { tw } from '$lib/tw';
import {
ArrowRightEndOnRectangle,
Bell,
Cog,
DevicePhoneMobile,
@ -11,22 +12,32 @@
QuestionMarkCircle,
UserPlus
} from 'svelte-hero-icons';
import { scale } from 'svelte/transition';
import SettingsListItem from './SettingsListItem.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
let className = '';
export { className as class };
</script>
<div class={tw('flex flex-col bg-sb-secondary shadow-md', className)}>
<div class="flex items-center border-b border-ss-secondary bg-sb-primary p-2 shadow-sm">
<div
class={tw('flex flex-col bg-sb-secondary shadow-md', className)}
transition:scale={{ duration: 250, start: 0.95 }}
>
<PageHeader>
<Button href="/">Done</Button>
<h2 class="flex-1 truncate text-center font-semibold text-sf-primary">Settings</h2>
</div>
<h2 class="flex-1 truncate text-center">Settings</h2>
<Button href="/account/profile">Edit</Button>
</PageHeader>
<div class="flex-1 overflow-y-scroll">
<GroupedListSection>
<GroupedListItem icon={UserPlus}>Add Account</GroupedListItem>
<SettingsListItem icon={ArrowRightEndOnRectangle} route="/account/add">
Sign in
</SettingsListItem>
<SettingsListItem icon={UserPlus} route="/account/new">Create Account</SettingsListItem>
</GroupedListSection>
<GroupedListSection>
<GroupedListItem icon={Cog} selected>General</GroupedListItem>
<SettingsListItem icon={Cog} route="">General</SettingsListItem>
<GroupedListItem icon={Bell}>Notifications</GroupedListItem>
<GroupedListItem icon={LockClosed}>Privacy and Security</GroupedListItem>
<GroupedListItem icon={DevicePhoneMobile}>Devices</GroupedListItem>

View file

@ -0,0 +1,15 @@
<script lang="ts">
import { page } from '$app/stores';
import { GroupedListItem } from '$lib/components/GroupedList';
import { type IconSource } from 'svelte-hero-icons';
export let icon: IconSource | undefined = undefined;
export let route: string | undefined = undefined;
$: selected = route
? $page.route.id?.startsWith(`/(app)/settings${route}`)
: $page.route.id === '/(app)/settings';
$: href = `/settings${route}`;
</script>
<GroupedListItem {icon} {selected} {href}><slot /></GroupedListItem>

View file

@ -0,0 +1,12 @@
<script lang="ts">
import { GroupedListContainer, GroupedListSection } from '$lib/components/GroupedList';
import PageHeader from '$lib/components/PageHeader.svelte';
</script>
<PageHeader><h3 class="flex-1">Sign in</h3></PageHeader>
<GroupedListContainer>
<GroupedListSection>
<p slot="footer">New here? <a class="text-accent-500" href="new">Create a new account</a>.</p>
</GroupedListSection>
</GroupedListContainer>

View file

@ -0,0 +1,129 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { openAccountStore } from '$lib/accounts/accountStore';
import Button from '$lib/components/Button.svelte';
import {
GroupedListContainer,
GroupedListSection,
GroupedListItem,
GroupedListInputItem
} from '$lib/components/GroupedList';
import LoadingIndicator from '$lib/components/LoadingIndicator.svelte';
import PageHeader from '$lib/components/PageHeader.svelte';
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
import RichTextInput from '$lib/components/RichTextInput.svelte';
import type { BlahProfile } from '@blah-im/core/identity';
import { onMount } from 'svelte';
import type { Delta, Editor } from 'typewriter-editor';
let name: string = '';
let editor: Editor | undefined;
let delta: Delta;
let plainText: string = '';
let password: string = '';
let repeatPassword: string = '';
let isBusy: boolean = false;
let bioPlaceholder = 'Introduce yourself.';
const bioPlaceholders = [
'a 23 yo. designer from Tokyo.',
'a 19 yo. student from New York.',
'a 30 yo. developer from Berlin.',
'a 25 yo. artist from Paris.',
'a 28 yo. writer from London.'
];
$: passwordMatch = password === repeatPassword;
$: canCreate = name.length > 0 && password.length > 0 && passwordMatch;
onMount(() => {
const bioPlaceholderRotateRef = setInterval(() => {
bioPlaceholder = bioPlaceholders[Math.floor(Math.random() * bioPlaceholders.length)];
}, 5000);
return () => clearInterval(bioPlaceholderRotateRef);
});
async function createAccount() {
const profile: BlahProfile = {
typ: 'profile',
name,
bio: plainText,
preferred_chat_server_urls: [],
id_urls: []
};
isBusy = true;
try {
const accountStore = await openAccountStore();
await accountStore.createAccount(profile, password);
} catch (error) {
console.error(error);
}
isBusy = false;
goto('/settings');
}
</script>
<PageHeader>
<h3 class="flex-1">Create Account</h3>
{#if isBusy}
<LoadingIndicator class="size-4" />
{:else}
<Button variant="primary" disabled={!canCreate} on:click={createAccount}>Create</Button>
{/if}
</PageHeader>
<GroupedListContainer>
<GroupedListSection>
<GroupedListItem>
<ProfilePicture size={64} account={undefined} />
<input
type="text"
bind:value={name}
placeholder="Your Name"
disabled={isBusy}
class="ms-3 flex-1 bg-transparent text-lg leading-loose caret-accent-500 outline-none placeholder:opacity-50"
/>
</GroupedListItem>
</GroupedListSection>
<GroupedListSection>
<h4 slot="header">Bio</h4>
<RichTextInput
class="p-4 shadow-none ring-0"
bind:editor
bind:delta
bind:plainText
placeholder={bioPlaceholder}
/>
<p slot="footer">Introduce yourself. This will be public for everyone to see.</p>
</GroupedListSection>
<GroupedListSection>
<h4 slot="header">Security</h4>
<GroupedListInputItem>
Password
<input type="password" bind:value={password} placeholder="Password" disabled={isBusy} />
</GroupedListInputItem>
<GroupedListInputItem>
Repeat Password
<input
type="password"
bind:value={repeatPassword}
placeholder="Repeat Password"
disabled={isBusy}
/>
</GroupedListInputItem>
<div slot="footer" class="space-y-1">
<p>
Sensitive actions like signing in on new devices require your password. Make sure it's
unique and secure. You'll lose access to your account if you forget it.
</p>
{#if !passwordMatch && repeatPassword}
<p><strong>Passwords do not match.</strong></p>
{/if}
</div>
</GroupedListSection>
</GroupedListContainer>