mirror of
https://github.com/lumehq/lume.git
synced 2025-03-26 01:31:48 +01:00
clean up & update edit profile modal
This commit is contained in:
parent
fe28cd95bd
commit
53227c7050
@ -18,7 +18,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ctrl/magnet-link": "^3.1.2",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@nostr-dev-kit/ndk": "^0.8.21",
|
||||
"@nostr-fetch/adapter-ndk": "^0.12.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||
|
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
@ -4,9 +4,6 @@ dependencies:
|
||||
'@ctrl/magnet-link':
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2
|
||||
'@headlessui/react':
|
||||
specifier: ^1.7.17
|
||||
version: 1.7.17(react-dom@18.2.0)(react@18.2.0)
|
||||
'@nostr-dev-kit/ndk':
|
||||
specifier: ^0.8.21
|
||||
version: 0.8.21(typescript@5.2.2)
|
||||
@ -872,18 +869,6 @@ packages:
|
||||
resolution: {integrity: sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==}
|
||||
dev: false
|
||||
|
||||
/@headlessui/react@1.7.17(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: ^16 || ^17 || ^18
|
||||
react-dom: ^16 || ^17 || ^18
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.11.10:
|
||||
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@ -3064,10 +3049,6 @@ packages:
|
||||
string-width: 5.1.2
|
||||
dev: true
|
||||
|
||||
/client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
dev: false
|
||||
|
||||
/clone-regexp@3.0.0:
|
||||
resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -6,7 +6,7 @@ import { User } from '@app/auth/components/user';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { CancelIcon, LoaderIcon, PlusIcon } from '@shared/icons';
|
||||
import { CancelIcon, PlusIcon } from '@shared/icons';
|
||||
|
||||
export function NewMessageModal() {
|
||||
const navigate = useNavigate();
|
||||
@ -54,29 +54,23 @@ export function NewMessageModal() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-2 pt-2">
|
||||
{status === 'loading' ? (
|
||||
<div className="inline-flex items-center justify-center px-4 py-3">
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
</div>
|
||||
) : (
|
||||
db.account?.follows?.map((follow) => (
|
||||
<div
|
||||
key={follow}
|
||||
className="group flex items-center justify-between px-4 py-2 hover:bg-white/10"
|
||||
>
|
||||
<User pubkey={follow} />
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openChat(follow)}
|
||||
className="hidden w-max rounded bg-white/10 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
|
||||
>
|
||||
Chat
|
||||
</button>
|
||||
</div>
|
||||
{db.account?.follows?.map((follow) => (
|
||||
<div
|
||||
key={follow}
|
||||
className="group flex items-center justify-between px-4 py-2 hover:bg-white/10"
|
||||
>
|
||||
<User pubkey={follow} />
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openChat(follow)}
|
||||
className="hidden w-max rounded bg-white/10 px-3 py-1 text-sm font-medium hover:bg-fuchsia-500 group-hover:inline-flex"
|
||||
>
|
||||
Chat
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
|
@ -1,56 +1,10 @@
|
||||
import { Switch } from '@headlessui/react';
|
||||
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function AutoStartSetting() {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
const toggle = async () => {
|
||||
if (!enabled) {
|
||||
await enable();
|
||||
// await updateSetting('auto_start', 1);
|
||||
console.log(`registered for autostart? ${await isEnabled()}`);
|
||||
} else {
|
||||
await disable();
|
||||
// await updateSetting('auto_start', 0);
|
||||
}
|
||||
setEnabled(!enabled);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function getAppSetting() {
|
||||
const setting = '0';
|
||||
if (parseInt(setting) === 0) {
|
||||
setEnabled(false);
|
||||
} else {
|
||||
setEnabled(true);
|
||||
}
|
||||
}
|
||||
getAppSetting();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center justify-between px-5 py-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium leading-none text-zinc-200">Auto start</span>
|
||||
<span className="text-sm leading-none text-white/50">Auto start at login</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={toggle}
|
||||
className={twMerge(
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-fuchsia-500 focus:ring-offset-2',
|
||||
enabled ? 'bg-fuchsia-500' : 'bg-zinc-700'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-zinc-900 shadow ring-0 transition duration-200 ease-in-out',
|
||||
enabled ? 'translate-x-5' : 'translate-x-0'
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,190 +0,0 @@
|
||||
import { Combobox } from '@headlessui/react';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { User } from '@app/auth/components/user';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { WidgetKinds, useWidgets } from '@stores/widgets';
|
||||
|
||||
export function FeedModal() {
|
||||
const setWidget = useWidgets((state) => state.setWidget);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const { db } = useStorage();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = (data: { kind: number; title: string; content: string }) => {
|
||||
setLoading(true);
|
||||
|
||||
selected.forEach((item, index) => {
|
||||
if (item.substring(0, 4) === 'npub') {
|
||||
selected[index] = nip19.decode(item).data;
|
||||
}
|
||||
});
|
||||
|
||||
// update state
|
||||
setWidget(db, {
|
||||
kind: WidgetKinds.feed,
|
||||
title: data.title,
|
||||
content: JSON.stringify(selected),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
// reset form
|
||||
reset();
|
||||
// close modal
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<CommandIcon className="h-3 w-3 text-white" />
|
||||
</div>
|
||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<span className="text-sm leading-none text-white">F</span>
|
||||
</div>
|
||||
</div>
|
||||
<h5 className="font-medium text-white/50">Add newsfeed widget</h5>
|
||||
</button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal className="relative z-10">
|
||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
|
||||
<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">
|
||||
<div className="h-min w-full shrink-0 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">
|
||||
Create feed block
|
||||
</Dialog.Title>
|
||||
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-white/10">
|
||||
<CancelIcon className="h-4 w-4 text-white/50" />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<Dialog.Description className="text-sm leading-tight text-white/50">
|
||||
Specific newsfeed space for people you want to keep up to date
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col overflow-y-auto overflow-x-hidden px-5 pb-5 pt-2">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="mb-0 flex h-full w-full flex-col gap-4"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="title"
|
||||
className="text-sm font-medium uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Title *
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('title', {
|
||||
required: true,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium uppercase tracking-wider text-white/50">
|
||||
Choose at least 1 user *
|
||||
</span>
|
||||
<div className="flex h-[300px] w-full flex-col overflow-y-auto overflow-x-hidden rounded-lg bg-white/10">
|
||||
<div className="w-full px-3 py-2">
|
||||
<Combobox value={selected} onChange={setSelected} multiple>
|
||||
<Combobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
spellCheck={false}
|
||||
placeholder="Enter pubkey or npub..."
|
||||
className="relative mb-2 h-10 w-full rounded-md bg-white/10 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
<Combobox.Options static>
|
||||
{query.length > 0 && (
|
||||
<Combobox.Option
|
||||
value={query}
|
||||
className="group flex w-full items-center justify-between rounded-md px-2 py-2 hover:bg-white/10"
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={query}
|
||||
className="h-11 w-11 shrink-0 rounded object-cover"
|
||||
/>
|
||||
<div className="inline-flex flex-col gap-1">
|
||||
<span className="text-base leading-tight text-white/50">
|
||||
{query}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{selected && (
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
)}
|
||||
{db.account?.follows?.map((follow) => (
|
||||
<Combobox.Option
|
||||
key={follow}
|
||||
value={follow}
|
||||
className="group flex w-full items-center justify-between rounded-md px-2 py-2 hover:bg-white/10"
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<User pubkey={follow} />
|
||||
{selected && (
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="shadow-button inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
||||
) : (
|
||||
'Confirm'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { WidgetKinds, useWidgets } from '@stores/widgets';
|
||||
|
||||
export function HashtagModal() {
|
||||
const setWidget = useWidgets((state) => state.setWidget);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { db } = useStorage();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const onSubmit = async (data: { hashtag: string }) => {
|
||||
setLoading(true);
|
||||
|
||||
// update state
|
||||
setWidget(db, {
|
||||
kind: WidgetKinds.hashtag,
|
||||
title: data.hashtag,
|
||||
content: data.hashtag.replace('#', ''),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
// reset form
|
||||
reset();
|
||||
// close modal
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-9 w-72 items-center justify-start gap-2.5 rounded-md px-2.5"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<CommandIcon className="h-3 w-3 text-white" />
|
||||
</div>
|
||||
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded bg-white/10">
|
||||
<span className="text-sm leading-none text-white">T</span>
|
||||
</div>
|
||||
</div>
|
||||
<h5 className="font-medium text-white/50">Add hashtag widget</h5>
|
||||
</button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal className="relative z-10">
|
||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
|
||||
<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">
|
||||
<div className="h-min w-full shrink-0 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">
|
||||
Create hashtag block
|
||||
</Dialog.Title>
|
||||
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-white/10">
|
||||
<CancelIcon className="h-4 w-4 text-white/50" />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<Dialog.Description className="text-sm leading-tight text-white/50">
|
||||
Pin the hashtag you want to keep follow up
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="mb-0 flex h-full w-full flex-col gap-3"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="title"
|
||||
className="text-sm font-medium uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Hashtag *
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('hashtag', {
|
||||
required: true,
|
||||
})}
|
||||
spellCheck={false}
|
||||
placeholder="#"
|
||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isDirty || !isValid}
|
||||
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||
) : (
|
||||
'Confirm'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
295
src/app/users/components/modal.tsx
Normal file
295
src/app/users/components/modal.tsx
Normal file
@ -0,0 +1,295 @@
|
||||
import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { AvatarUploader } from '@shared/avatarUploader';
|
||||
import { BannerUploader } from '@shared/bannerUploader';
|
||||
import { CancelIcon, CheckCircleIcon, LoaderIcon, UnverifiedIcon } from '@shared/icons';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function EditProfileModal() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [picture, setPicture] = useState('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
|
||||
const [banner, setBanner] = useState(null);
|
||||
const [nip05, setNIP05] = useState({ verified: false, text: '' });
|
||||
|
||||
const { db } = useStorage();
|
||||
const { publish } = useNostr();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setError,
|
||||
formState: { isValid, errors },
|
||||
} = useForm({
|
||||
defaultValues: async () => {
|
||||
const res: NDKUserProfile = queryClient.getQueryData(['user', db.account.pubkey]);
|
||||
if (res.image) {
|
||||
setPicture(res.image);
|
||||
}
|
||||
if (res.banner) {
|
||||
setBanner(res.banner);
|
||||
}
|
||||
if (res.nip05) {
|
||||
setNIP05((prev) => ({ ...prev, text: res.nip05 }));
|
||||
}
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
const verifyNIP05 = async (nip05: string) => {
|
||||
const url = nip05.split('@');
|
||||
const username = url[0];
|
||||
const service = url[1];
|
||||
const verifyURL = `https://${service}/.well-known/nostr.json?name=${username}`;
|
||||
|
||||
const res = await fetch(verifyURL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
});
|
||||
|
||||
console.log(res);
|
||||
return true;
|
||||
};
|
||||
|
||||
const onSubmit = async (data: NDKUserProfile) => {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
let event: NDKEvent;
|
||||
|
||||
const content = {
|
||||
...data,
|
||||
username: data.name,
|
||||
display_name: data.name,
|
||||
bio: data.about,
|
||||
image: data.picture,
|
||||
};
|
||||
|
||||
if (data.nip05) {
|
||||
const nip05IsVerified = await verifyNIP05(data.nip05);
|
||||
if (nip05IsVerified) {
|
||||
event = await publish({
|
||||
content: JSON.stringify({ ...content, nip05: data.nip05 }),
|
||||
kind: 0,
|
||||
tags: [],
|
||||
});
|
||||
} else {
|
||||
setNIP05((prev) => ({ ...prev, verified: false }));
|
||||
setError('nip05', {
|
||||
type: 'manual',
|
||||
message: "Can't verify your Lume ID / NIP-05, please check again",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
event = await publish({
|
||||
content: JSON.stringify(content),
|
||||
kind: 0,
|
||||
tags: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (event.id) {
|
||||
// invalid cache
|
||||
queryClient.invalidateQueries(['user', db.account.pubkey]);
|
||||
// reset form
|
||||
reset();
|
||||
// reset state
|
||||
setLoading(false);
|
||||
setIsOpen(false);
|
||||
setPicture('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
|
||||
setBanner(null);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!nip05.verified && /\S+@\S+\.\S+/.test(nip05.text)) {
|
||||
verifyNIP05(nip05.text);
|
||||
}
|
||||
}, [nip05.text]);
|
||||
|
||||
return (
|
||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
||||
<Dialog.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
|
||||
>
|
||||
Edit profile
|
||||
</button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal className="relative z-10">
|
||||
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/80 backdrop-blur-xl" />
|
||||
<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">
|
||||
<div className="h-min w-full shrink-0 border-b border-white/10 bg-white/5 px-5 py-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title className="text-lg font-semibold leading-none text-white">
|
||||
Edit profile
|
||||
</Dialog.Title>
|
||||
<Dialog.Close className="inline-flex h-6 w-6 items-center justify-center rounded-md hover:bg-white/10">
|
||||
<CancelIcon className="h-4 w-4 text-white/50" />
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0">
|
||||
<input type={'hidden'} {...register('picture')} value={picture} />
|
||||
<input type={'hidden'} {...register('banner')} value={banner} />
|
||||
<div className="relative">
|
||||
<div className="relative h-44 w-full bg-zinc-800">
|
||||
{banner ? (
|
||||
<img
|
||||
src={banner}
|
||||
alt="user's banner"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full w-full bg-white" />
|
||||
)}
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<BannerUploader setBanner={setBanner} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-5 px-4">
|
||||
<div className="relative z-10 -mt-7 h-14 w-14">
|
||||
<Image
|
||||
src={picture}
|
||||
alt="user's avatar"
|
||||
className="h-14 w-14 rounded-lg object-cover ring-2 ring-zinc-900"
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<AvatarUploader setPicture={setPicture} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('name', {
|
||||
required: true,
|
||||
minLength: 4,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="nip05"
|
||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||
>
|
||||
NIP-05
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register('nip05', {
|
||||
required: true,
|
||||
minLength: 4,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform">
|
||||
{nip05.verified ? (
|
||||
<span className="inline-flex h-6 items-center gap-1 rounded bg-green-500 px-2 text-sm font-medium">
|
||||
<CheckCircleIcon className="h-4 w-4 text-white" />
|
||||
Verified
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex h-6 items-center gap-1 rounded bg-red-500 px-2 text-sm font-medium">
|
||||
<UnverifiedIcon className="h-4 w-4 text-white" />
|
||||
Unverified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{errors.nip05 && (
|
||||
<p className="mt-1 text-sm text-red-400">
|
||||
{errors.nip05.message.toString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="about"
|
||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register('about')}
|
||||
spellCheck={false}
|
||||
className="relative h-20 w-full resize-none rounded-lg bg-white/10 px-3 py-1 !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="website"
|
||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('website', { required: false })}
|
||||
spellCheck={false}
|
||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="website"
|
||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Lightning address
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('lud16', { required: false })}
|
||||
spellCheck={false}
|
||||
className="relative h-11 w-full rounded-lg bg-white/10 px-3 py-1 !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
className="inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
||||
) : (
|
||||
'Update'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { EditProfileModal } from '@app/users/components/modal';
|
||||
import { UserStats } from '@app/users/components/stats';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { EditProfileModal } from '@shared/editProfileModal';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
@ -7,9 +7,7 @@ import { useImageUploader } from '@utils/hooks/useUploader';
|
||||
export function AvatarUploader({
|
||||
setPicture,
|
||||
}: {
|
||||
setPicture: Dispatch<
|
||||
SetStateAction<{ url: undefined | string; error?: undefined | string }>
|
||||
>;
|
||||
setPicture: Dispatch<SetStateAction<string>>;
|
||||
}) {
|
||||
const upload = useImageUploader();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
@ -1,347 +0,0 @@
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { AvatarUploader } from '@shared/avatarUploader';
|
||||
import { BannerUploader } from '@shared/bannerUploader';
|
||||
import { CancelIcon, CheckCircleIcon, LoaderIcon, UnverifiedIcon } from '@shared/icons';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function EditProfileModal() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [picture, setPicture] = useState('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
|
||||
const [banner, setBanner] = useState('');
|
||||
const [nip05, setNIP05] = useState({ verified: false, text: '' });
|
||||
|
||||
const { db } = useStorage();
|
||||
const { publish } = useNostr();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setError,
|
||||
formState: { isValid, errors },
|
||||
} = useForm({
|
||||
defaultValues: async () => {
|
||||
const res: any = queryClient.getQueryData(['user', db.account.pubkey]);
|
||||
if (res.image) {
|
||||
setPicture(res.image);
|
||||
}
|
||||
if (res.banner) {
|
||||
setBanner(res.banner);
|
||||
}
|
||||
if (res.nip05) {
|
||||
setNIP05((prev) => ({ ...prev, text: res.nip05 }));
|
||||
}
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const verifyNIP05 = async (data: string) => {
|
||||
if (data) {
|
||||
const url = data.split('@');
|
||||
const username = url[0];
|
||||
const service = url[1];
|
||||
const verifyURL = `https://${service}/.well-known/nostr.json?name=${username}`;
|
||||
|
||||
const res: any = await fetch(verifyURL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) return false;
|
||||
if (res.data.names[username] === db.account.pubkey) {
|
||||
setNIP05((prev) => ({ ...prev, verified: true }));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
let event: NDKEvent;
|
||||
|
||||
const content = {
|
||||
...data,
|
||||
username: data.name,
|
||||
display_name: data.name,
|
||||
bio: data.about,
|
||||
image: data.picture,
|
||||
};
|
||||
|
||||
if (data.nip05) {
|
||||
const verify = await verifyNIP05(data.nip05);
|
||||
if (verify) {
|
||||
event = await publish({
|
||||
content: JSON.stringify({ ...content, nip05: data.nip05 }),
|
||||
kind: 0,
|
||||
tags: [],
|
||||
});
|
||||
} else {
|
||||
setNIP05((prev) => ({ ...prev, verified: false }));
|
||||
setError('nip05', {
|
||||
type: 'manual',
|
||||
message: "Can't verify your Lume ID / NIP-05, please check again",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
event = await publish({
|
||||
content: JSON.stringify(content),
|
||||
kind: 0,
|
||||
tags: [],
|
||||
});
|
||||
}
|
||||
|
||||
if (event.id) {
|
||||
setTimeout(() => {
|
||||
// invalid cache
|
||||
queryClient.invalidateQueries(['user', db.account.pubkey]);
|
||||
// reset form
|
||||
reset();
|
||||
// reset state
|
||||
setLoading(false);
|
||||
setIsOpen(false);
|
||||
setPicture('https://void.cat/d/5VKmKyuHyxrNMf9bWSVPih');
|
||||
setBanner(null);
|
||||
}, 1200);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!nip05.verified && /\S+@\S+\.\S+/.test(nip05.text)) {
|
||||
verifyNIP05(nip05.text);
|
||||
}
|
||||
}, [nip05.text]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openModal()}
|
||||
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium hover:bg-fuchsia-500"
|
||||
>
|
||||
Edit profile
|
||||
</button>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col rounded-lg border-t border-zinc-800/50 bg-zinc-900">
|
||||
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-semibold leading-none text-white"
|
||||
>
|
||||
Edit profile
|
||||
</Dialog.Title>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||
>
|
||||
<CancelIcon className="h-5 w-5 text-zinc-300" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0">
|
||||
<input
|
||||
type={'hidden'}
|
||||
{...register('picture')}
|
||||
value={picture}
|
||||
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-white/50 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-white/50"
|
||||
/>
|
||||
<input
|
||||
type={'hidden'}
|
||||
{...register('banner')}
|
||||
value={banner}
|
||||
className="shadow-input relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-white/50 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-white/50"
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="relative h-44 w-full bg-zinc-800">
|
||||
<img
|
||||
src={banner}
|
||||
alt="user's banner"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<BannerUploader setBanner={setBanner} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-5 px-4">
|
||||
<div className="relative z-10 -mt-7 h-14 w-14">
|
||||
<Image
|
||||
src={picture}
|
||||
alt="user's avatar"
|
||||
className="h-14 w-14 rounded-lg object-cover ring-2 ring-zinc-900"
|
||||
/>
|
||||
<div className="absolute left-1/2 top-1/2 z-10 h-full w-full -translate-x-1/2 -translate-y-1/2 transform">
|
||||
<AvatarUploader setPicture={setPicture} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('name', {
|
||||
required: true,
|
||||
minLength: 4,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="nip05"
|
||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Lume ID / NIP-05
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
{...register('nip05', {
|
||||
required: true,
|
||||
minLength: 4,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
<div className="absolute right-2 top-1/2 -translate-y-1/2 transform">
|
||||
{nip05.verified ? (
|
||||
<span className="inline-flex h-6 items-center gap-1 rounded bg-green-500 px-2 text-sm font-medium">
|
||||
<CheckCircleIcon className="h-4 w-4 text-white" />
|
||||
Verified
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex h-6 items-center gap-1 rounded bg-red-500 px-2 text-sm font-medium">
|
||||
<UnverifiedIcon className="h-4 w-4 text-white" />
|
||||
Unverified
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{errors.nip05 && (
|
||||
<p className="mt-1 text-sm text-red-400">
|
||||
{errors.nip05.message.toString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="about"
|
||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register('about')}
|
||||
spellCheck={false}
|
||||
className="relative h-20 w-full resize-none rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="website"
|
||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('website', { required: false })}
|
||||
spellCheck={false}
|
||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="website"
|
||||
className="text-sm font-semibold uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Lightning address
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('lud16', { required: false })}
|
||||
spellCheck={false}
|
||||
className="relative h-10 w-full rounded-lg bg-zinc-800 px-3 py-2 text-white !outline-none placeholder:text-white/50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
className="inline-flex h-11 w-full transform items-center justify-center gap-1 rounded-md bg-fuchsia-500 font-medium text-white hover:bg-fuchsia-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-white" />
|
||||
) : (
|
||||
'Update'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user