polish
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
import { AddFeedBlock } from '@app/space/components/addFeed';
|
||||
import { AddHashTagBlock } from '@app/space/components/addHashtag';
|
||||
import { AddImageBlock } from '@app/space/components/addImage';
|
||||
|
||||
export function AddBlock() {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<AddImageBlock />
|
||||
<AddFeedBlock />
|
||||
<AddHashTagBlock />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { Combobox } from '@headlessui/react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { User } from '@app/auth/components/user';
|
||||
|
||||
import { createBlock } from '@libs/storage';
|
||||
|
||||
import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
|
||||
import { ADD_FEEDBLOCK_SHORTCUT } from '@stores/shortcuts';
|
||||
|
||||
import { useAccount } from '@utils/hooks/useAccount';
|
||||
|
||||
export function AddFeedBlock() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const { status, account } = useAccount();
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useHotkeys(ADD_FEEDBLOCK_SHORTCUT, () => openModal());
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: (data: { kind: number; title: string; content: string }) => {
|
||||
return createBlock(data.kind, data.title, data.content);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// insert to database
|
||||
block.mutate({
|
||||
kind: BLOCK_KINDS.feed,
|
||||
title: data.title,
|
||||
content: JSON.stringify(selected),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
// reset form
|
||||
reset();
|
||||
// close modal
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openModal()}
|
||||
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 width={12} height={12} className="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>
|
||||
<div>
|
||||
<h5 className="font-medium text-white/50">New feed block</h5>
|
||||
</div>
|
||||
</button>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" 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 gap-2 rounded-xl 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 flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-semibold leading-none text-zinc-100"
|
||||
>
|
||||
Create feed block
|
||||
</Dialog.Title>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||
>
|
||||
<CancelIcon width={14} height={14} className="text-zinc-300" />
|
||||
</button>
|
||||
</div>
|
||||
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
||||
Specific newsfeed space for people you want to keep up to date
|
||||
</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-4"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="title"
|
||||
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
|
||||
>
|
||||
Title *
|
||||
</label>
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('title', {
|
||||
required: true,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-10 w-full rounded-md bg-zinc-800 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
||||
Choose at least 1 user *
|
||||
</span>
|
||||
<div className="flex h-[300px] w-full flex-col overflow-y-auto overflow-x-hidden rounded-lg border-t border-zinc-700/50 bg-zinc-800">
|
||||
<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-zinc-700 px-3 py-2 text-zinc-100 !outline-none placeholder:text-zinc-500"
|
||||
/>
|
||||
<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-zinc-700"
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
alt={query}
|
||||
src={DEFAULT_AVATAR}
|
||||
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-zinc-400">
|
||||
{query}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{selected && (
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
)}
|
||||
{status === 'loading' ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
JSON.parse(account.follows as string).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-zinc-700"
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<User pubkey={follow} />
|
||||
{selected && (
|
||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))
|
||||
)}
|
||||
</Combobox.Options>
|
||||
</Combobox>
|
||||
</div>
|
||||
</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-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
'Confirm'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { createBlock } from '@libs/storage';
|
||||
|
||||
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { BLOCK_KINDS } from '@stores/constants';
|
||||
import { ADD_HASHTAGBLOCK_SHORTCUT } from '@stores/shortcuts';
|
||||
|
||||
export function AddHashTagBlock() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useHotkeys(ADD_HASHTAGBLOCK_SHORTCUT, () => openModal());
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: (data: { kind: number; title: string; content: string }) => {
|
||||
return createBlock(data.kind, data.title, data.content);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: { hashtag: string }) => {
|
||||
setLoading(true);
|
||||
|
||||
// mutate
|
||||
block.mutate({
|
||||
kind: BLOCK_KINDS.hashtag,
|
||||
title: data.hashtag,
|
||||
content: data.hashtag.replace('#', ''),
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
// reset form
|
||||
reset();
|
||||
// close modal
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openModal()}
|
||||
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 width={12} height={12} className="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>
|
||||
<div>
|
||||
<h5 className="font-medium text-white/50">New hashtag block</h5>
|
||||
</div>
|
||||
</button>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" 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 gap-2 rounded-xl 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 flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-semibold leading-none text-zinc-100"
|
||||
>
|
||||
Create hashtag block
|
||||
</Dialog.Title>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||
>
|
||||
<CancelIcon width={14} height={14} className="text-zinc-300" />
|
||||
</button>
|
||||
</div>
|
||||
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
||||
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-4"
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="title"
|
||||
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
|
||||
>
|
||||
Hashtag *
|
||||
</label>
|
||||
<div className="after:shadow-highlight relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[6px] after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('hashtag', {
|
||||
required: true,
|
||||
})}
|
||||
spellCheck={false}
|
||||
placeholder="#"
|
||||
className="shadow-input relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||
/>
|
||||
</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-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
'Confirm'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { createBlock } from '@libs/storage';
|
||||
|
||||
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
|
||||
import { ADD_IMAGEBLOCK_SHORTCUT } from '@stores/shortcuts';
|
||||
|
||||
import { usePublish } from '@utils/hooks/usePublish';
|
||||
import { useImageUploader } from '@utils/hooks/useUploader';
|
||||
|
||||
export function AddImageBlock() {
|
||||
const queryClient = useQueryClient();
|
||||
const upload = useImageUploader();
|
||||
|
||||
const { publish } = usePublish();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [image, setImage] = useState('');
|
||||
|
||||
const tags = useRef(null);
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useHotkeys(ADD_IMAGEBLOCK_SHORTCUT, () => openModal());
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: (data: { kind: number; title: string; content: string }) => {
|
||||
return createBlock(data.kind, data.title, data.content);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||
},
|
||||
});
|
||||
|
||||
const uploadImage = async () => {
|
||||
const image = await upload(null);
|
||||
if (image.url) {
|
||||
setImage(image.url);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data: { kind: number; title: string; content: string }) => {
|
||||
setLoading(true);
|
||||
|
||||
// publish file metedata
|
||||
await publish({ content: data.title, kind: 1063, tags: tags.current });
|
||||
|
||||
// mutate
|
||||
block.mutate({ kind: BLOCK_KINDS.image, title: data.title, content: data.content });
|
||||
|
||||
setLoading(false);
|
||||
// reset form
|
||||
reset();
|
||||
// close modal
|
||||
closeModal();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setValue('content', image);
|
||||
}, [setValue, image]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openModal()}
|
||||
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 width={12} height={12} className="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">I</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-medium text-white/50">New image block</h5>
|
||||
</div>
|
||||
</button>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-50" 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 gap-2 rounded-xl 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 flex-col gap-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-semibold leading-none text-zinc-100"
|
||||
>
|
||||
Create image block
|
||||
</Dialog.Title>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||
>
|
||||
<CancelIcon width={14} height={14} className="text-zinc-300" />
|
||||
</button>
|
||||
</div>
|
||||
<Dialog.Description className="text-sm leading-tight text-zinc-400">
|
||||
Pin your favorite image to Space then you can view every time that
|
||||
you use Lume, your image will be broadcast to Nostr Relay as well
|
||||
</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-4"
|
||||
>
|
||||
<input
|
||||
type={'hidden'}
|
||||
{...register('content')}
|
||||
value={image}
|
||||
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-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="title"
|
||||
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
|
||||
>
|
||||
Title *
|
||||
</label>
|
||||
<div className="after:shadow-highlight relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[6px] after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
|
||||
<input
|
||||
type={'text'}
|
||||
{...register('title', {
|
||||
required: true,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="shadow-input relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label
|
||||
htmlFor="picture"
|
||||
className="text-sm font-medium uppercase tracking-wider text-zinc-400"
|
||||
>
|
||||
Picture
|
||||
</label>
|
||||
<div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
||||
<Image
|
||||
src={image}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt="content"
|
||||
className="relative z-10 h-auto max-h-[156px] w-[150px] rounded-md object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3 z-10">
|
||||
<button
|
||||
onClick={() => uploadImage()}
|
||||
type="button"
|
||||
className="inline-flex h-6 items-center justify-center rounded bg-zinc-900 px-3 text-sm font-medium text-zinc-300 ring-1 ring-zinc-800 hover:bg-zinc-800"
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</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-zinc-100 active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
'Confirm'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -121,15 +121,15 @@ export function FeedBlock({ params }: { params: Block }) {
|
||||
<div className="h-full">
|
||||
{status === 'loading' ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
|
||||
<div className="rounded-xl bg-white/10 px-3 py-3">
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
) : itemsVirtualizer.length === 0 ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-6">
|
||||
<div className="bbg-white/10 rounded-xl px-3 py-6">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<p className="text-center text-sm text-zinc-300">
|
||||
<p className="text-center text-sm text-white">
|
||||
Not found any posts from last 48 hours
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -133,15 +133,15 @@ export function FollowingBlock() {
|
||||
<div className="h-full">
|
||||
{status === 'loading' ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
|
||||
<div className="rounded-xl bg-white/10 px-3 py-3">
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
) : itemsVirtualizer.length === 0 ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-6">
|
||||
<div className="rounded-xl bg-white/10 px-3 py-6">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<p className="text-center text-sm text-zinc-300">
|
||||
<p className="text-center text-sm text-white">
|
||||
You not have any posts to see yet
|
||||
<br />
|
||||
Follow more people to have more fun.
|
||||
|
||||
@@ -39,15 +39,15 @@ export function HashtagBlock({ params }: { params: Block }) {
|
||||
<div className="h-full">
|
||||
{status === 'loading' ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
|
||||
<div className="rounded-xl bg-white/10 px-3 pt-3">
|
||||
<NoteSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
) : itemsVirtualizer.length === 0 ? (
|
||||
<div className="px-3 py-1.5">
|
||||
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-6">
|
||||
<div className="rounded-xl bg-white/10 px-3 py-6">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<p className="text-center text-sm text-zinc-300">
|
||||
<p className="text-center text-sm text-white">
|
||||
No new posts about this hashtag in 48 hours ago
|
||||
</p>
|
||||
</div>
|
||||
|
||||
212
src/app/space/components/modals/feed.tsx
Normal file
212
src/app/space/components/modals/feed.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import { Combobox } from '@headlessui/react';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { User } from '@app/auth/components/user';
|
||||
|
||||
import { createBlock } from '@libs/storage';
|
||||
|
||||
import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
|
||||
import { ADD_FEEDBLOCK_SHORTCUT } from '@stores/shortcuts';
|
||||
|
||||
import { useAccount } from '@utils/hooks/useAccount';
|
||||
|
||||
export function FeedModal() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
const { status, account } = useAccount();
|
||||
|
||||
useHotkeys(ADD_FEEDBLOCK_SHORTCUT, () => setOpen(true));
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: (data: { kind: number; title: string; content: string }) => {
|
||||
return createBlock(data.kind, data.title, data.content);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
// insert to database
|
||||
block.mutate({
|
||||
kind: BLOCK_KINDS.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">New feed block</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}
|
||||
src={DEFAULT_AVATAR}
|
||||
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>
|
||||
)}
|
||||
{status === 'loading' ? (
|
||||
<p>Loading...</p>
|
||||
) : (
|
||||
JSON.parse(account.follows as string).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>
|
||||
);
|
||||
}
|
||||
132
src/app/space/components/modals/hashtag.tsx
Normal file
132
src/app/space/components/modals/hashtag.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { createBlock } from '@libs/storage';
|
||||
|
||||
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||
|
||||
import { BLOCK_KINDS } from '@stores/constants';
|
||||
import { ADD_HASHTAGBLOCK_SHORTCUT } from '@stores/shortcuts';
|
||||
|
||||
export function HashtagModal() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useHotkeys(ADD_HASHTAGBLOCK_SHORTCUT, () => setOpen(false));
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: (data: { kind: number; title: string; content: string }) => {
|
||||
return createBlock(data.kind, data.title, data.content);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: { hashtag: string }) => {
|
||||
setLoading(true);
|
||||
|
||||
// mutate
|
||||
block.mutate({
|
||||
kind: BLOCK_KINDS.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">New hashtag block</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>
|
||||
);
|
||||
}
|
||||
184
src/app/space/components/modals/image.tsx
Normal file
184
src/app/space/components/modals/image.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { createBlock } from '@libs/storage';
|
||||
|
||||
import { CancelIcon, CommandIcon, LoaderIcon } from '@shared/icons';
|
||||
import { Image } from '@shared/image';
|
||||
|
||||
import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants';
|
||||
import { ADD_IMAGEBLOCK_SHORTCUT } from '@stores/shortcuts';
|
||||
|
||||
import { usePublish } from '@utils/hooks/usePublish';
|
||||
import { useImageUploader } from '@utils/hooks/useUploader';
|
||||
|
||||
export function ImageModal() {
|
||||
const queryClient = useQueryClient();
|
||||
const upload = useImageUploader();
|
||||
|
||||
const { publish } = usePublish();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [image, setImage] = useState('');
|
||||
|
||||
const tags = useRef(null);
|
||||
|
||||
useHotkeys(ADD_IMAGEBLOCK_SHORTCUT, () => setOpen(false));
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { isDirty, isValid },
|
||||
} = useForm();
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: (data: { kind: number; title: string; content: string }) => {
|
||||
return createBlock(data.kind, data.title, data.content);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['blocks'] });
|
||||
},
|
||||
});
|
||||
|
||||
const uploadImage = async () => {
|
||||
const image = await upload(null);
|
||||
if (image.url) {
|
||||
setImage(image.url);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data: { kind: number; title: string; content: string }) => {
|
||||
setLoading(true);
|
||||
|
||||
// publish file metedata
|
||||
await publish({ content: data.title, kind: 1063, tags: tags.current });
|
||||
|
||||
// mutate
|
||||
block.mutate({ kind: BLOCK_KINDS.image, title: data.title, content: data.content });
|
||||
|
||||
setLoading(false);
|
||||
// reset form
|
||||
reset();
|
||||
// close modal
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setValue('content', image);
|
||||
}, [setValue, image]);
|
||||
|
||||
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 width={12} height={12} className="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">I</span>
|
||||
</div>
|
||||
</div>
|
||||
<h5 className="font-medium text-white/50">New image block</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 image 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 your favorite image to Space then you can view every time that you
|
||||
use Lume, your image will be broadcast to Nostr Relay as well
|
||||
</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"
|
||||
>
|
||||
<input
|
||||
type={'hidden'}
|
||||
{...register('content')}
|
||||
value={image}
|
||||
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-zinc-500"
|
||||
/>
|
||||
<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">
|
||||
<label
|
||||
htmlFor="picture"
|
||||
className="text-sm font-medium uppercase tracking-wider text-white/50"
|
||||
>
|
||||
Picture
|
||||
</label>
|
||||
<div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
||||
<Image
|
||||
src={image}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt="content"
|
||||
className="relative z-10 h-auto max-h-[156px] w-[150px] rounded-md object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3 z-10">
|
||||
<button
|
||||
onClick={() => uploadImage()}
|
||||
type="button"
|
||||
className="inline-flex h-6 items-center justify-center rounded bg-zinc-900 px-3 text-sm font-medium text-zinc-300 ring-1 ring-zinc-800 hover:bg-zinc-800"
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { AddBlock } from '@app/space/components/add';
|
||||
import { FeedBlock } from '@app/space/components/blocks/feed';
|
||||
import { FollowingBlock } from '@app/space/components/blocks/following';
|
||||
import { HashtagBlock } from '@app/space/components/blocks/hashtag';
|
||||
import { ImageBlock } from '@app/space/components/blocks/image';
|
||||
import { ThreadBlock } from '@app/space/components/blocks/thread';
|
||||
import { UserBlock } from '@app/space/components/blocks/user';
|
||||
import { FeedModal } from '@app/space/components/modals/feed';
|
||||
import { HashtagModal } from '@app/space/components/modals/hashtag';
|
||||
import { ImageModal } from '@app/space/components/modals/image';
|
||||
|
||||
import { getBlocks } from '@libs/storage';
|
||||
|
||||
@@ -76,13 +78,15 @@ export function SpaceScreen() {
|
||||
className="group flex h-11 w-full items-center justify-between overflow-hidden px-3"
|
||||
/>
|
||||
<div className="flex w-full flex-1 items-center justify-center p-3">
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-black dark:text-zinc-100" />
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-white" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-[350px] shrink-0 flex-col">
|
||||
<div className="inline-flex h-full w-full items-center justify-center">
|
||||
<AddBlock />
|
||||
<div className="inline-flex h-full w-full flex-col items-center justify-center gap-1">
|
||||
<FeedModal />
|
||||
<ImageModal />
|
||||
<HashtagModal />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[250px] shrink-0" />
|
||||
|
||||
Reference in New Issue
Block a user