mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-01 00:31:08 +00:00
feat: create account ui
This commit is contained in:
parent
e3e3481739
commit
2b47eeb146
14 changed files with 303 additions and 17 deletions
|
@ -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]',
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<div class="mx-auto max-w-3xl"><slot /></div>
|
|
@ -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>
|
|
@ -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'
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
|
|
95
src/lib/components/LoadingIndicator.svelte
Normal file
95
src/lib/components/LoadingIndicator.svelte
Normal 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>
|
15
src/lib/components/PageHeader.svelte
Normal file
15
src/lib/components/PageHeader.svelte
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
15
src/routes/(app)/settings/SettingsListItem.svelte
Normal file
15
src/routes/(app)/settings/SettingsListItem.svelte
Normal 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>
|
12
src/routes/(app)/settings/account/add/+page.svelte
Normal file
12
src/routes/(app)/settings/account/add/+page.svelte
Normal 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>
|
129
src/routes/(app)/settings/account/new/+page.svelte
Normal file
129
src/routes/(app)/settings/account/new/+page.svelte
Normal 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>
|
Loading…
Add table
Reference in a new issue