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}
|
{href}
|
||||||
class={tw(
|
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',
|
'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' && [
|
variant === 'primary' && [
|
||||||
'relative text-slate-50 ring-0 duration-200',
|
'relative text-slate-50 ring-0 duration-200',
|
||||||
'before:absolute before:-inset-px before:rounded-[7px]',
|
'before:absolute before:-inset-px before:rounded-[7px]',
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import GroupedListItem from './GroupedList/GroupedListItem.svelte';
|
import GroupedListItem from './GroupedList/GroupedListItem.svelte';
|
||||||
|
import GroupedListInputItem from './GroupedList/GroupedListInputItem.svelte';
|
||||||
import GroupedListSection from './GroupedList/GroupedListSection.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'}
|
this={href ? 'a' : 'div'}
|
||||||
{href}
|
{href}
|
||||||
class={tw(
|
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'
|
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}
|
{#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" />
|
<slot name="header" />
|
||||||
</h3>
|
</h3>
|
||||||
{/if}
|
{/if}
|
||||||
<div
|
<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 />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{#if $$slots.footer}
|
{#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" />
|
<slot name="footer" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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, {
|
const db = await openDB<IdentityFileDBSchema>(IDB_NAME, 1, {
|
||||||
upgrade(db) {
|
upgrade(db) {
|
||||||
if (!db.objectStoreNames.contains(IDB_OBJECT_STORE_NAME)) {
|
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', {
|
store.createIndex('id_urls', 'profile.signee.payload.id_urls', {
|
||||||
multiEntry: true,
|
multiEntry: true,
|
||||||
unique: true
|
unique: true
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
import SettingsList from './settings/SettingsList.svelte';
|
import SettingsList from './settings/SettingsList.svelte';
|
||||||
|
|
||||||
onNavigate((navigation) => {
|
onNavigate((navigation) => {
|
||||||
if (!document.startViewTransition) return;
|
if (!document.startViewTransition || navigation.from?.url.href === navigation.to?.url.href)
|
||||||
|
return;
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
document.startViewTransition(async () => {
|
document.startViewTransition(async () => {
|
||||||
|
@ -15,7 +16,7 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$: isSettings = $page.route.id?.startsWith('/(app)/settings');
|
$: isSettings = $page.route.id?.startsWith('/(app)/settings') ?? true;
|
||||||
$: mainVisible =
|
$: mainVisible =
|
||||||
!!$page.params.chatId ||
|
!!$page.params.chatId ||
|
||||||
(isSettings && !$page.route.id?.startsWith('/(app)/settings/_mobile_empty'));
|
(isSettings && !$page.route.id?.startsWith('/(app)/settings/_mobile_empty'));
|
||||||
|
@ -30,7 +31,7 @@
|
||||||
>
|
>
|
||||||
<ChatList />
|
<ChatList />
|
||||||
{#if isSettings}
|
{#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}
|
{/if}
|
||||||
</aside>
|
</aside>
|
||||||
{#if mainVisible}
|
{#if mainVisible}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { GroupedListSection, GroupedListItem } from '$lib/components/GroupedList';
|
import { GroupedListSection, GroupedListItem } from '$lib/components/GroupedList';
|
||||||
import { tw } from '$lib/tw';
|
import { tw } from '$lib/tw';
|
||||||
import {
|
import {
|
||||||
|
ArrowRightEndOnRectangle,
|
||||||
Bell,
|
Bell,
|
||||||
Cog,
|
Cog,
|
||||||
DevicePhoneMobile,
|
DevicePhoneMobile,
|
||||||
|
@ -11,22 +12,32 @@
|
||||||
QuestionMarkCircle,
|
QuestionMarkCircle,
|
||||||
UserPlus
|
UserPlus
|
||||||
} from 'svelte-hero-icons';
|
} from 'svelte-hero-icons';
|
||||||
|
import { scale } from 'svelte/transition';
|
||||||
|
import SettingsListItem from './SettingsListItem.svelte';
|
||||||
|
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||||
|
|
||||||
let className = '';
|
let className = '';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={tw('flex flex-col bg-sb-secondary shadow-md', className)}>
|
<div
|
||||||
<div class="flex items-center border-b border-ss-secondary bg-sb-primary p-2 shadow-sm">
|
class={tw('flex flex-col bg-sb-secondary shadow-md', className)}
|
||||||
|
transition:scale={{ duration: 250, start: 0.95 }}
|
||||||
|
>
|
||||||
|
<PageHeader>
|
||||||
<Button href="/">Done</Button>
|
<Button href="/">Done</Button>
|
||||||
<h2 class="flex-1 truncate text-center font-semibold text-sf-primary">Settings</h2>
|
<h2 class="flex-1 truncate text-center">Settings</h2>
|
||||||
</div>
|
<Button href="/account/profile">Edit</Button>
|
||||||
|
</PageHeader>
|
||||||
<div class="flex-1 overflow-y-scroll">
|
<div class="flex-1 overflow-y-scroll">
|
||||||
<GroupedListSection>
|
<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>
|
||||||
<GroupedListSection>
|
<GroupedListSection>
|
||||||
<GroupedListItem icon={Cog} selected>General</GroupedListItem>
|
<SettingsListItem icon={Cog} route="">General</SettingsListItem>
|
||||||
<GroupedListItem icon={Bell}>Notifications</GroupedListItem>
|
<GroupedListItem icon={Bell}>Notifications</GroupedListItem>
|
||||||
<GroupedListItem icon={LockClosed}>Privacy and Security</GroupedListItem>
|
<GroupedListItem icon={LockClosed}>Privacy and Security</GroupedListItem>
|
||||||
<GroupedListItem icon={DevicePhoneMobile}>Devices</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