mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 11:43:30 +00:00
clean up & update edit profile modal
This commit is contained in:
parent
fe28cd95bd
commit
53227c7050
@ -18,7 +18,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctrl/magnet-link": "^3.1.2",
|
"@ctrl/magnet-link": "^3.1.2",
|
||||||
"@headlessui/react": "^1.7.17",
|
|
||||||
"@nostr-dev-kit/ndk": "^0.8.21",
|
"@nostr-dev-kit/ndk": "^0.8.21",
|
||||||
"@nostr-fetch/adapter-ndk": "^0.12.2",
|
"@nostr-fetch/adapter-ndk": "^0.12.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.4",
|
"@radix-ui/react-alert-dialog": "^1.0.4",
|
||||||
|
@ -4,9 +4,6 @@ dependencies:
|
|||||||
'@ctrl/magnet-link':
|
'@ctrl/magnet-link':
|
||||||
specifier: ^3.1.2
|
specifier: ^3.1.2
|
||||||
version: 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':
|
'@nostr-dev-kit/ndk':
|
||||||
specifier: ^0.8.21
|
specifier: ^0.8.21
|
||||||
version: 0.8.21(typescript@5.2.2)
|
version: 0.8.21(typescript@5.2.2)
|
||||||
@ -872,18 +869,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==}
|
resolution: {integrity: sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==}
|
||||||
dev: false
|
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:
|
/@humanwhocodes/config-array@0.11.10:
|
||||||
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
@ -3064,10 +3049,6 @@ packages:
|
|||||||
string-width: 5.1.2
|
string-width: 5.1.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/client-only@0.0.1:
|
|
||||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/clone-regexp@3.0.0:
|
/clone-regexp@3.0.0:
|
||||||
resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==}
|
resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
@ -6,7 +6,7 @@ import { User } from '@app/auth/components/user';
|
|||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { CancelIcon, LoaderIcon, PlusIcon } from '@shared/icons';
|
import { CancelIcon, PlusIcon } from '@shared/icons';
|
||||||
|
|
||||||
export function NewMessageModal() {
|
export function NewMessageModal() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -54,29 +54,23 @@ export function NewMessageModal() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-2 pt-2">
|
<div className="flex h-[500px] flex-col overflow-y-auto overflow-x-hidden pb-2 pt-2">
|
||||||
{status === 'loading' ? (
|
{db.account?.follows?.map((follow) => (
|
||||||
<div className="inline-flex items-center justify-center px-4 py-3">
|
<div
|
||||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
key={follow}
|
||||||
</div>
|
className="group flex items-center justify-between px-4 py-2 hover:bg-white/10"
|
||||||
) : (
|
>
|
||||||
db.account?.follows?.map((follow) => (
|
<User pubkey={follow} />
|
||||||
<div
|
<div>
|
||||||
key={follow}
|
<button
|
||||||
className="group flex items-center justify-between px-4 py-2 hover:bg-white/10"
|
type="button"
|
||||||
>
|
onClick={() => openChat(follow)}
|
||||||
<User pubkey={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"
|
||||||
<div>
|
>
|
||||||
<button
|
Chat
|
||||||
type="button"
|
</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>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</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() {
|
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 (
|
return (
|
||||||
<div className="inline-flex items-center justify-between px-5 py-4">
|
<div className="inline-flex items-center justify-between px-5 py-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className="font-medium leading-none text-zinc-200">Auto start</span>
|
<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>
|
<span className="text-sm leading-none text-white/50">Auto start at login</span>
|
||||||
</div>
|
</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>
|
</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 { useEffect, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { EditProfileModal } from '@app/users/components/modal';
|
||||||
import { UserStats } from '@app/users/components/stats';
|
import { UserStats } from '@app/users/components/stats';
|
||||||
|
|
||||||
import { useStorage } from '@libs/storage/provider';
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
import { EditProfileModal } from '@shared/editProfileModal';
|
|
||||||
import { Image } from '@shared/image';
|
import { Image } from '@shared/image';
|
||||||
|
|
||||||
import { useNostr } from '@utils/hooks/useNostr';
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
@ -7,9 +7,7 @@ import { useImageUploader } from '@utils/hooks/useUploader';
|
|||||||
export function AvatarUploader({
|
export function AvatarUploader({
|
||||||
setPicture,
|
setPicture,
|
||||||
}: {
|
}: {
|
||||||
setPicture: Dispatch<
|
setPicture: Dispatch<SetStateAction<string>>;
|
||||||
SetStateAction<{ url: undefined | string; error?: undefined | string }>
|
|
||||||
>;
|
|
||||||
}) {
|
}) {
|
||||||
const upload = useImageUploader();
|
const upload = useImageUploader();
|
||||||
const [loading, setLoading] = useState(false);
|
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…
Reference in New Issue
Block a user