wip: redesign nwc

This commit is contained in:
Ren Amamiya 2023-09-12 08:27:29 +07:00
parent 602d012efe
commit a4f221f868
8 changed files with 283 additions and 35 deletions

View File

@ -80,6 +80,13 @@ const router = createBrowserRouter([
return { Component: NotificationScreen };
},
},
{
path: 'nwc',
async lazy() {
const { NWCScreen } = await import('@app/nwc');
return { Component: NWCScreen };
},
},
],
},
{

View File

@ -12,19 +12,19 @@ import {
CancelIcon,
CheckCircleIcon,
LoaderIcon,
StarsIcon,
} from '@shared/icons';
import { useStronghold } from '@stores/stronghold';
export function AlbyConnectButton() {
export function NWCAlby() {
const { db } = useStorage();
const setWalletConnectURL = useStronghold((state) => state.setWalletConnectURL);
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsloading] = useState(false);
const [isConnected, setIsConnected] = useState(false);
const setWalletConnectURL = useStronghold((state) => state.setWalletConnectURL);
const initAlby = async () => {
try {
setIsloading(true);
@ -58,31 +58,24 @@ export function AlbyConnectButton() {
return (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<div className="relative w-full rounded-xl bg-gradient-to-r from-orange-400 via-red-200 to-yellow-200 p-px">
<StarsIcon className="absolute -left-4 -top-3 z-50 h-10 w-10 text-white" />
<div className="flex w-full flex-col rounded-xl bg-white/10 backdrop-blur-xl">
<div className="absolute right-2 top-2">
<button type="button">
<CancelIcon className="h-4 w-4 text-black/50" />
</button>
<div className="flex items-center justify-between">
<div className="inline-flex items-center gap-2">
<div className="inline-flex h-11 w-11 items-center justify-center rounded-md bg-orange-200">
<AlbyIcon className="h-8 w-8" />
</div>
<div className="flex h-14 w-full flex-col items-center justify-center">
<h5 className="text-center text-sm font-semibold leading-tight text-black/50">
New feature
</h5>
<h3 className="transform font-medium leading-tight text-black">
Send bitcoin tip with Alby
</h3>
<div>
<h5 className="font-semibold leading-tight text-white">Alby</h5>
<p className="text-sm leading-tight text-white/50">Require alby account</p>
</div>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-10 w-full shrink-0 items-center justify-center gap-1 rounded-b-xl border-t border-orange-200 bg-white text-sm font-semibold text-orange-400 hover:bg-orange-50"
>
Connect your Alby account <AlbyIcon className="h-7 w-7" />
</button>
</Dialog.Trigger>
</div>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-8 w-min items-center justify-center rounded-md bg-white/10 px-2.5 text-sm font-medium text-white hover:bg-white/20"
>
Connect
</button>
</Dialog.Trigger>
</div>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />

View File

@ -0,0 +1,166 @@
import * as Dialog from '@radix-ui/react-dialog';
import { useState } from 'react';
import { Resolver, useForm } from 'react-hook-form';
import { useStorage } from '@libs/storage/provider';
import { ArrowRightCircleIcon, CancelIcon, LoaderIcon, WorldIcon } from '@shared/icons';
import { useStronghold } from '@stores/stronghold';
type FormValues = {
uri: string;
};
const resolver: Resolver<FormValues> = async (values) => {
return {
values: values.uri ? values : {},
errors: !values.uri
? {
uri: {
type: 'required',
message: 'This is required.',
},
}
: {},
};
};
export function NWCOther() {
const { db } = useStorage();
const {
register,
setError,
handleSubmit,
formState: { errors, isDirty, isValid },
} = useForm<FormValues>({ resolver });
const [isOpen, setIsOpen] = useState(false);
const [isLoading, setIsloading] = useState(false);
const setWalletConnectURL = useStronghold((state) => state.setWalletConnectURL);
const onSubmit = async (data: { [x: string]: string }) => {
try {
if (!data.uri.startsWith('nostr+walletconnect:')) {
setError('uri', {
type: 'custom',
message:
'Connect URI is required and must start with format nostr+walletconnect:, please check again',
});
return;
}
setIsloading(true);
const uriObj = new URL(data.uri);
const params = new URLSearchParams(uriObj.search);
if (params.has('relay') && params.has('secret')) {
await db.secureSave('walletConnectURL', data.uri, 'alby');
setWalletConnectURL(data.uri);
setIsloading(false);
}
} catch (e) {
setIsloading(false);
setError('uri', {
type: 'custom',
message:
'Connect URI is required and must start with format nostr+walletconnect:, please check again',
});
}
};
return (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<div className="flex items-center justify-between pt-3">
<div className="inline-flex items-center gap-2">
<div className="inline-flex h-11 w-11 items-center justify-center rounded-md bg-white/10">
<WorldIcon className="h-5 w-5" />
</div>
<div>
<h5 className="font-semibold leading-tight text-white">URI String</h5>
<p className="text-sm leading-tight text-white/50">
Using format nostr+walletconnect://
</p>
</div>
</div>
<Dialog.Trigger asChild>
<button
type="button"
className="inline-flex h-8 w-min items-center justify-center rounded-md bg-white/10 px-2.5 text-sm font-medium text-white hover:bg-white/20"
>
Connect
</button>
</Dialog.Trigger>
</div>
<Dialog.Portal className="relative z-10">
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-2xl" />
<Dialog.Content className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="relative h-min w-full max-w-xl rounded-xl bg-white/10 backdrop-blur-xl">
<div className="h-min w-full shrink-0 rounded-t-xl border-b border-white/10 bg-white/5 px-5 py-5">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<Dialog.Title className="text-lg font-semibold leading-none text-white">
Nostr Wallet Connect
</Dialog.Title>
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md backdrop-blur-xl hover:bg-white/10">
<CancelIcon className="h-4 w-4 text-white/50" />
</Dialog.Close>
</div>
</div>
</div>
<form
onSubmit={handleSubmit(onSubmit)}
className="mb-0 flex flex-col gap-3 px-5 py-5"
>
<div className="flex flex-col gap-1">
<label
htmlFor="uri"
className="text-sm font-semibold uppercase tracking-wider text-white/50"
>
Connect URI
</label>
<input
{...register('uri', { required: true })}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 text-white !outline-none backdrop-blur-xl placeholder:text-white/50"
/>
<span className="text-sm text-red-400">
{errors.uri && <p>{errors.uri.message}</p>}
</span>
</div>
<div className="flex flex-col gap-1 text-center">
<button
type="submit"
disabled={!isDirty || !isValid}
className="inline-flex h-11 w-full items-center justify-between gap-2 rounded-lg bg-fuchsia-500 px-6 font-medium leading-none text-white hover:bg-fuchsia-600 focus:outline-none disabled:opacity-50"
>
{isLoading ? (
<>
<span className="w-5" />
<span>Connecting...</span>
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
</>
) : (
<>
<span className="w-5" />
<span>Connect</span>
<ArrowRightCircleIcon className="h-5 w-5" />
</>
)}
</button>
<span className="text-sm text-white/50">
All information will be encrypted and stored on the local machine.
</span>
</div>
</form>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

21
src/app/nwc/index.tsx Normal file
View File

@ -0,0 +1,21 @@
import { NWCAlby } from '@app/nwc/components/alby';
import { NWCOther } from '@app/nwc/components/other';
export function NWCScreen() {
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex w-full flex-col gap-5">
<div className="text-center">
<h3 className="text-2xl font-bold leading-tight">Nostr Wallet Connect</h3>
<p className="leading-tight text-white/50">
Sending tips easily via Bitcoin Lightning.
</p>
</div>
<div className="mx-auto flex w-full max-w-lg flex-col gap-3 divide-y divide-white/5 rounded-xl bg-white/10 p-3">
<NWCAlby />
<NWCOther />
</div>
</div>
</div>
);
}

View File

@ -63,3 +63,4 @@ export * from './article';
export * from './follows';
export * from './alby';
export * from './stars';
export * from './nwc';

22
src/shared/icons/nwc.tsx Normal file
View File

@ -0,0 +1,22 @@
import { SVGProps } from 'react';
export function NwcIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M3 6.5A3.5 3.5 0 016.5 3h8.969C16.314 3 17 3.686 17 4.531V8h2.25c.966 0 1.75.784 1.75 1.75v9.5A1.75 1.75 0 0119.25 21h-7a.75.75 0 010-1.5h7a.25.25 0 00.25-.25v-9.5a.25.25 0 00-.25-.25H6a2.986 2.986 0 01-1.5-.401v2.151a.75.75 0 01-1.5 0V6.5zm1.5 0A1.5 1.5 0 006 8h9.5V4.531a.031.031 0 00-.031-.031H6.5a2 2 0 00-2 2zm-1 9.285v2.93L6 20.173l2.5-1.458v-2.93L6 14.327l-2.5 1.458zm2.122-2.975a.75.75 0 01.756 0l3.25 1.896a.75.75 0 01.372.648v3.792a.75.75 0 01-.372.648l-3.25 1.895a.75.75 0 01-.756 0l-3.25-1.895A.75.75 0 012 19.146v-3.792a.75.75 0 01.372-.648l3.25-1.896z"
clipRule="evenodd"
></path>
<path fill="currentColor" d="M15.5 15.5a1 1 0 100-2 1 1 0 000 2z"></path>
</svg>
);
}

View File

@ -7,23 +7,24 @@ import { ChatsList } from '@app/chats/components/list';
import { useStorage } from '@libs/storage/provider';
import { ActiveAccount } from '@shared/accounts/active';
import { AlbyConnectButton } from '@shared/alby';
import { ComposerModal } from '@shared/composer';
import { Frame } from '@shared/frame';
import { BellIcon, NavArrowDownIcon, SpaceIcon } from '@shared/icons';
import { BellIcon, NavArrowDownIcon, NwcIcon, SpaceIcon } from '@shared/icons';
import { useActivities } from '@stores/activities';
import { useSidebar } from '@stores/sidebar';
import { useStronghold } from '@stores/stronghold';
import { compactNumber } from '@utils/number';
export function Navigation() {
const { db } = useStorage();
const walletConnectURL = useStronghold((state) => state.walletConnectURL);
const [totalNewActivities] = useActivities((state) => [state.totalNewActivities]);
const [chats, toggleChats] = useSidebar((state) => [state.chats, state.toggleChats]);
const [integrations, toggleIntegrations] = useSidebar((state) => [
state.integrations,
state.toggleIntegrations,
]);
return (
<Frame className="relative flex h-full w-[232px] flex-col" lighter>
@ -77,6 +78,44 @@ export function Navigation() {
) : null}
</NavLink>
</div>
<Collapsible.Root open={integrations} onOpenChange={toggleIntegrations}>
<div className="flex flex-col gap-1 pr-2">
<Collapsible.Trigger asChild>
<button className="flex items-center gap-1 pl-[20px] pr-4">
<div
className={twMerge(
'inline-flex h-5 w-5 transform items-center justify-center transition-transform duration-150 ease-in-out',
integrations ? '' : 'rotate-180'
)}
>
<NavArrowDownIcon className="h-3 w-3 text-white/50" />
</div>
<h3 className="text-[11px] font-bold uppercase tracking-widest text-white/50">
Integrations
</h3>
</button>
</Collapsible.Trigger>
<Collapsible.Content>
<NavLink
to="/nwc"
preventScrollReset={true}
className={({ isActive }) =>
twMerge(
'flex h-10 items-center gap-2.5 rounded-r-lg border-l-2 pl-4 pr-2',
isActive
? 'border-fuchsia-500 bg-white/5 text-white'
: 'border-transparent text-white/80'
)
}
>
<span className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded bg-white/10 backdrop-blur-xl">
<NwcIcon className="h-4 w-4 text-white" />
</span>
Wallet Connect
</NavLink>
</Collapsible.Content>
</div>
</Collapsible.Root>
<Collapsible.Root open={chats} onOpenChange={toggleChats}>
<div className="flex flex-col gap-1 pr-2">
<Collapsible.Trigger asChild>
@ -101,11 +140,6 @@ export function Navigation() {
</Collapsible.Root>
</div>
<div className="relative shrink-0">
{!walletConnectURL ? (
<div className="border-l-2 border-transparent pb-2 pl-4 pr-2">
<AlbyConnectButton />
</div>
) : null}
<ActiveAccount />
</div>
</Frame>

View File

@ -4,8 +4,10 @@ import { createJSONStorage, persist } from 'zustand/middleware';
interface SidebarState {
feeds: boolean;
chats: boolean;
integrations: boolean;
toggleFeeds: () => void;
toggleChats: () => void;
toggleIntegrations: () => void;
}
export const useSidebar = create<SidebarState>()(
@ -13,12 +15,14 @@ export const useSidebar = create<SidebarState>()(
(set) => ({
feeds: true,
chats: true,
integrations: true,
toggleFeeds: () => set((state) => ({ feeds: !state.feeds })),
toggleChats: () => set((state) => ({ chats: !state.chats })),
toggleIntegrations: () => set((state) => ({ integrations: !state.integrations })),
}),
{
name: 'sidebar',
storage: createJSONStorage(() => sessionStorage),
storage: createJSONStorage(() => localStorage),
}
)
);