mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-19 19:46:34 +00:00
add create space
This commit is contained in:
parent
f61adb2ff5
commit
dffe300a5f
67
src/app/space/components/add.tsx
Normal file
67
src/app/space/components/add.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { AddFeedBlock } from "@app/space/components/addFeed";
|
||||||
|
import { AddImageBlock } from "@app/space/components/addImage";
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
import { FeedIcon, ImageIcon, PlusIcon } from "@shared/icons";
|
||||||
|
import { Fragment, useState } from "react";
|
||||||
|
|
||||||
|
export function AddBlock() {
|
||||||
|
const [imageModal, setImageModal] = useState(false);
|
||||||
|
const [feedModal, setFeedModal] = useState(false);
|
||||||
|
|
||||||
|
const openAddImageModal = () => {
|
||||||
|
setImageModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddFeedModal = () => {
|
||||||
|
setFeedModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
|
<Menu.Button className="group inline-flex flex-col items-center gap-2.5">
|
||||||
|
<div className="inline-flex h-9 w-9 shrink items-center justify-center rounded-lg bg-zinc-900 group-hover:bg-zinc-800">
|
||||||
|
<PlusIcon width={16} height={16} className="text-zinc-500" />
|
||||||
|
</div>
|
||||||
|
</Menu.Button>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="absolute mt-2 right-1/2 transform translate-x-1/2 w-56 origin-top-right rounded-md bg-zinc-900 ring-1 ring-zinc-800 focus:outline-none">
|
||||||
|
<div className="px-1 py-1">
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openAddImageModal()}
|
||||||
|
className="group flex w-full items-center rounded-md hover:bg-zinc-800 text-zinc-300 hover:text-zinc-100 px-2 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<ImageIcon width={15} height={15} className="mr-2" />
|
||||||
|
Add image
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => openAddFeedModal()}
|
||||||
|
className="group flex w-full items-center rounded-md hover:bg-zinc-800 text-zinc-300 hover:text-zinc-100 px-2 py-2 text-sm"
|
||||||
|
>
|
||||||
|
<FeedIcon width={15} height={15} className="mr-2" />
|
||||||
|
Add feed
|
||||||
|
</button>
|
||||||
|
</Menu.Item>
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
{imageModal && <AddImageBlock parentState={setImageModal} />}
|
||||||
|
{feedModal && <AddFeedBlock parentState={setFeedModal} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
179
src/app/space/components/addFeed.tsx
Normal file
179
src/app/space/components/addFeed.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { CancelIcon } from "@shared/icons";
|
||||||
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
|
import { createBlock } from "@utils/storage";
|
||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
import { Fragment, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
export function AddFeedBlock({ parentState }: { parentState: any }) {
|
||||||
|
const account = useActiveAccount((state: any) => state.account);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
// update local state
|
||||||
|
setIsOpen(false);
|
||||||
|
// update parent state
|
||||||
|
parentState(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isDirty, isValid },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const onSubmit = (data: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
let pubkey = data.content;
|
||||||
|
|
||||||
|
if (pubkey.substring(0, 4) === "npub") {
|
||||||
|
pubkey = nip19.decode(pubkey).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert to database
|
||||||
|
createBlock(account.id, 1, data.title, pubkey);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
// reset form
|
||||||
|
reset();
|
||||||
|
// close modal
|
||||||
|
closeModal();
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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-lg border border-zinc-800 bg-zinc-900">
|
||||||
|
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-6">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-semibold leading-none text-white"
|
||||||
|
>
|
||||||
|
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="flex h-full w-full flex-col gap-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
||||||
|
Title *
|
||||||
|
</label>
|
||||||
|
<div className="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-highlight 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="relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
||||||
|
Pubkey OR Npub *
|
||||||
|
</label>
|
||||||
|
<div className="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-highlight 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("content", {
|
||||||
|
required: true,
|
||||||
|
})}
|
||||||
|
spellCheck={false}
|
||||||
|
className="relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
|
/>
|
||||||
|
</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 shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4 animate-spin text-black dark:text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<title id="loading">Loading</title>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
"Confirm"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
}
|
265
src/app/space/components/addImage.tsx
Normal file
265
src/app/space/components/addImage.tsx
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
import { CancelIcon, ImageIcon } from "@shared/icons";
|
||||||
|
import { Image } from "@shared/image";
|
||||||
|
import { RelayContext } from "@shared/relayProvider";
|
||||||
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
|
import { WRITEONLY_RELAYS } from "@stores/constants";
|
||||||
|
import { open } from "@tauri-apps/api/dialog";
|
||||||
|
import { Body, fetch } from "@tauri-apps/api/http";
|
||||||
|
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
||||||
|
import { dateToUnix } from "@utils/date";
|
||||||
|
import { createBlock } from "@utils/storage";
|
||||||
|
import { getEventHash, getSignature } from "nostr-tools";
|
||||||
|
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
export function AddImageBlock({ parentState }: { parentState: any }) {
|
||||||
|
const pool: any = useContext(RelayContext);
|
||||||
|
const account = useActiveAccount((state: any) => state.account);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
const [image, setImage] = useState("");
|
||||||
|
|
||||||
|
const tags = useRef(null);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
// update local state
|
||||||
|
setIsOpen(false);
|
||||||
|
// update parent state
|
||||||
|
parentState(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
setValue,
|
||||||
|
formState: { isDirty, isValid },
|
||||||
|
} = useForm();
|
||||||
|
|
||||||
|
const openFileDialog = async () => {
|
||||||
|
const selected: any = await open({
|
||||||
|
multiple: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "Image",
|
||||||
|
extensions: ["png", "jpeg", "jpg"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(selected)) {
|
||||||
|
// user selected multiple files
|
||||||
|
} else if (selected === null) {
|
||||||
|
// user cancelled the selection
|
||||||
|
} else {
|
||||||
|
const filename = selected.split("/").pop();
|
||||||
|
const file = await createBlobFromFile(selected);
|
||||||
|
const buf = await file.arrayBuffer();
|
||||||
|
|
||||||
|
const res: any = await fetch("https://void.cat/upload?cli=false", {
|
||||||
|
method: "POST",
|
||||||
|
timeout: 5,
|
||||||
|
headers: {
|
||||||
|
accept: "*/*",
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
"V-Filename": filename,
|
||||||
|
"V-Description": "Upload from https://lume.nu",
|
||||||
|
"V-Strip-Metadata": "true",
|
||||||
|
},
|
||||||
|
body: Body.bytes(buf),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const imageURL = `https://void.cat/d/${res.data.file.id}.webp`;
|
||||||
|
tags.current = [
|
||||||
|
["url", imageURL],
|
||||||
|
["m", res.data.file.metadata.mimeType],
|
||||||
|
["x", res.data.file.metadata.digest],
|
||||||
|
["size", res.data.file.metadata.size],
|
||||||
|
["magnet", res.data.file.metadata.magnetLink],
|
||||||
|
];
|
||||||
|
|
||||||
|
setImage(imageURL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = (data: any) => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const event: any = {
|
||||||
|
content: data.title,
|
||||||
|
created_at: dateToUnix(),
|
||||||
|
kind: 1063,
|
||||||
|
pubkey: account.pubkey,
|
||||||
|
tags: tags.current,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(event);
|
||||||
|
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
event.sig = getSignature(event, account.privkey);
|
||||||
|
|
||||||
|
// publish channel
|
||||||
|
pool.publish(event, WRITEONLY_RELAYS);
|
||||||
|
|
||||||
|
// insert to database
|
||||||
|
createBlock(account.id, 0, data.title, data.content);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
// reset form
|
||||||
|
reset();
|
||||||
|
// close modal
|
||||||
|
closeModal();
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue("content", image);
|
||||||
|
}, [setValue, image]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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-lg border border-zinc-800 bg-zinc-900">
|
||||||
|
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-6">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg font-semibold leading-none text-white"
|
||||||
|
>
|
||||||
|
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="flex h-full w-full flex-col gap-4"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type={"hidden"}
|
||||||
|
{...register("content")}
|
||||||
|
value={image}
|
||||||
|
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
|
||||||
|
Title *
|
||||||
|
</label>
|
||||||
|
<div className="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-highlight 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="relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<label 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}
|
||||||
|
alt="content"
|
||||||
|
className="relative z-10 max-h-[156px] h-auto w-[150px] object-cover rounded-md"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-3 right-3 z-10">
|
||||||
|
<button
|
||||||
|
onClick={() => openFileDialog()}
|
||||||
|
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="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4 animate-spin text-black dark:text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<title id="loading">Loading</title>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
"Confirm"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
}
|
@ -1,216 +0,0 @@
|
|||||||
import { BlockImageUploader } from "./imageUploader";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
|
||||||
import { CancelIcon, PlusIcon } from "@shared/icons";
|
|
||||||
import { Image } from "@shared/image";
|
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
|
||||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
|
||||||
import { createBlock } from "@utils/storage";
|
|
||||||
import { Fragment, useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
export function CreateBlockModal() {
|
|
||||||
const account = useActiveAccount((state: any) => state.account);
|
|
||||||
const { register, handleSubmit, reset, watch, setValue } = useForm();
|
|
||||||
|
|
||||||
const [image, setImage] = useState(DEFAULT_AVATAR);
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const kind = watch("kind");
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openModal = () => {
|
|
||||||
setIsOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (data: any) => {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
createBlock(account.id, data.kind, data.title, data.content).then(() => {
|
|
||||||
// reset form
|
|
||||||
reset();
|
|
||||||
// close modal
|
|
||||||
setIsOpen(false);
|
|
||||||
// stop loading
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValue("content", image);
|
|
||||||
}, [setValue, image]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => openModal()}
|
|
||||||
className="group inline-flex flex-col items-center gap-2.5 p-4 rounded-md hover:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<div className="inline-flex h-5 w-5 shrink items-center justify-center rounded bg-zinc-900 group-hover:bg-zinc-800">
|
|
||||||
<PlusIcon width={12} height={12} className="text-zinc-500" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h5 className="font-semibold text-zinc-400 group-hover:text-zinc-200">
|
|
||||||
Create a new block
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</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 gap-2 rounded-lg border border-zinc-800 bg-zinc-900">
|
|
||||||
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-6">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Dialog.Title
|
|
||||||
as="h3"
|
|
||||||
className="text-xl font-semibold leading-none text-white"
|
|
||||||
>
|
|
||||||
Create 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={20}
|
|
||||||
height={20}
|
|
||||||
className="text-zinc-300"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Dialog.Description className="leading-tight text-zinc-300">
|
|
||||||
Personalize your space by adding a new block.
|
|
||||||
</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="flex h-full w-full flex-col gap-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label className="text-base font-semibold uppercase tracking-wider text-zinc-400">
|
|
||||||
Type *
|
|
||||||
</label>
|
|
||||||
<div className="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-[7px] after:shadow-highlight 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("kind", {
|
|
||||||
required: true,
|
|
||||||
})}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label className="text-base font-semibold uppercase tracking-wider text-zinc-400">
|
|
||||||
Title *
|
|
||||||
</label>
|
|
||||||
<div className="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-[7px] after:shadow-highlight 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,
|
|
||||||
minLength: 4,
|
|
||||||
})}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<label className="text-base font-semibold uppercase tracking-wider text-zinc-400">
|
|
||||||
Content *
|
|
||||||
</label>
|
|
||||||
{kind === "1" ? (
|
|
||||||
<div className="relative h-20 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-[7px] after:shadow-highlight 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">
|
|
||||||
<textarea
|
|
||||||
{...register("content", {
|
|
||||||
required: true,
|
|
||||||
})}
|
|
||||||
spellCheck={false}
|
|
||||||
className="relative h-20 w-full resize-none rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-white dark:shadow-black/10 dark:placeholder:text-zinc-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="relative inline-flex h-36 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
|
|
||||||
<Image
|
|
||||||
src={image}
|
|
||||||
alt="block featured image"
|
|
||||||
className="relative z-10 h-11 w-11 rounded-md"
|
|
||||||
/>
|
|
||||||
<div className="absolute bottom-3 right-3 z-10">
|
|
||||||
<BlockImageUploader valueState={setImage} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-white shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 animate-spin text-black dark:text-white"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<title id="loading">Loading</title>
|
|
||||||
<circle
|
|
||||||
className="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
"Create block"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
|
||||||
|
|
||||||
import { open } from "@tauri-apps/api/dialog";
|
|
||||||
import { Body, fetch } from "@tauri-apps/api/http";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export function BlockImageUploader({ valueState }: { valueState: any }) {
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const openFileDialog = async () => {
|
|
||||||
const selected: any = await open({
|
|
||||||
multiple: false,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "Image",
|
|
||||||
extensions: ["png", "jpeg", "jpg"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
if (Array.isArray(selected)) {
|
|
||||||
// user selected multiple files
|
|
||||||
} else if (selected === null) {
|
|
||||||
// user cancelled the selection
|
|
||||||
} else {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const filename = selected.split("/").pop();
|
|
||||||
const file = await createBlobFromFile(selected);
|
|
||||||
const buf = await file.arrayBuffer();
|
|
||||||
|
|
||||||
const res: { data: { file: { id: string } } } = await fetch(
|
|
||||||
"https://void.cat/upload?cli=false",
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
timeout: 5,
|
|
||||||
headers: {
|
|
||||||
accept: "*/*",
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
"V-Filename": filename,
|
|
||||||
"V-Description": "Upload from https://lume.nu",
|
|
||||||
"V-Strip-Metadata": "true",
|
|
||||||
},
|
|
||||||
body: Body.bytes(buf),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const webpImage = `https://void.cat/d/${res.data.file.id}.webp`;
|
|
||||||
|
|
||||||
valueState(webpImage);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => openFileDialog()}
|
|
||||||
type="button"
|
|
||||||
className="inline-flex h-6 items-center justify-center rounded bg-zinc-900 px-3 text-base font-medium text-white ring-1 ring-zinc-800 hover:bg-zinc-700"
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 animate-spin text-black dark:text-white"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<title id="loading">Loading</title>
|
|
||||||
<circle
|
|
||||||
className="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
className="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<span className="leading-none">Upload</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
|
import { AddBlock } from "@app/space/components/add";
|
||||||
import { FeedBlock } from "@app/space/components/blocks/feed";
|
import { FeedBlock } from "@app/space/components/blocks/feed";
|
||||||
import { FollowingBlock } from "@app/space/components/blocks/following";
|
import { FollowingBlock } from "@app/space/components/blocks/following";
|
||||||
import { ImageBlock } from "@app/space/components/blocks/image";
|
import { ImageBlock } from "@app/space/components/blocks/image";
|
||||||
import { CreateBlockModal } from "@app/space/components/create";
|
|
||||||
import { useActiveAccount } from "@stores/accounts";
|
import { useActiveAccount } from "@stores/accounts";
|
||||||
import { getBlocks } from "@utils/storage";
|
import { getBlocks } from "@utils/storage";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -16,7 +16,7 @@ export function Page() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden">
|
<div className="h-full w-full flex flex-nowrap overflow-x-auto overflow-y-hidden scrollbar-hide">
|
||||||
<FollowingBlock />
|
<FollowingBlock />
|
||||||
{data
|
{data
|
||||||
? data.map((block: any) =>
|
? data.map((block: any) =>
|
||||||
@ -27,9 +27,9 @@ export function Page() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
<div className="shrink-0 w-[360px] border-r border-zinc-900">
|
<div className="shrink-0 w-[90px]">
|
||||||
<div className="w-full h-full inline-flex items-center justify-center">
|
<div className="w-full h-full inline-flex items-center justify-center">
|
||||||
<CreateBlockModal />
|
<AddBlock />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 w-[360px]" />
|
<div className="shrink-0 w-[360px]" />
|
||||||
|
24
src/shared/icons/feed.tsx
Normal file
24
src/shared/icons/feed.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
|
export function FeedIcon(
|
||||||
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M19.5 8.75V11.5M19.5 11.5V14.25M19.5 11.5H16.75M19.5 11.5H22.25M14.75 6.5C14.75 8.57107 13.0711 10.25 11 10.25C8.92893 10.25 7.25 8.57107 7.25 6.5C7.25 4.42893 8.92893 2.75 11 2.75C13.0711 2.75 14.75 4.42893 14.75 6.5ZM3.5 20.25C3.86894 16.3254 6.8098 13.25 11 13.25C15.1902 13.25 18.1311 16.3254 18.5 20.25H3.5Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
21
src/shared/icons/image.tsx
Normal file
21
src/shared/icons/image.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
|
export function ImageIcon(
|
||||||
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3.75 3.75V3C3.33579 3 3 3.33579 3 3.75H3.75ZM20.25 3.75H21C21 3.33579 20.6642 3 20.25 3V3.75ZM20.25 20.25V21C20.6642 21 21 20.6642 21 20.25H20.25ZM3.75 20.25H3C3 20.6642 3.33579 21 3.75 21V20.25ZM3.23598 15.4538C2.93435 15.7377 2.91996 16.2124 3.20385 16.514C3.48774 16.8157 3.96239 16.83 4.26402 16.5462L3.23598 15.4538ZM8 12L8.53033 11.4697C8.24369 11.183 7.78117 11.176 7.48598 11.4538L8 12ZM12 16L11.4697 16.5303C11.7626 16.8232 12.2374 16.8232 12.5303 16.5303L12 16ZM14 14L14.5303 13.4697C14.2374 13.1768 13.7626 13.1768 13.4697 13.4697L14 14ZM19.4697 20.5303C19.7626 20.8232 20.2374 20.8232 20.5303 20.5303C20.8232 20.2374 20.8232 19.7626 20.5303 19.4697L19.4697 20.5303ZM3.75 4.5H20.25V3H3.75V4.5ZM19.5 3.75V20.25H21V3.75H19.5ZM20.25 19.5H3.75V21H20.25V19.5ZM4.5 20.25V3.75H3V20.25H4.5ZM4.26402 16.5462L8.51402 12.5462L7.48598 11.4538L3.23598 15.4538L4.26402 16.5462ZM7.46967 12.5303L11.4697 16.5303L12.5303 15.4697L8.53033 11.4697L7.46967 12.5303ZM12.5303 16.5303L14.5303 14.5303L13.4697 13.4697L11.4697 15.4697L12.5303 16.5303ZM13.4697 14.5303L19.4697 20.5303L20.5303 19.4697L14.5303 13.4697L13.4697 14.5303ZM15 9C15 9.41421 14.6642 9.75 14.25 9.75V11.25C15.4926 11.25 16.5 10.2426 16.5 9H15ZM14.25 9.75C13.8358 9.75 13.5 9.41421 13.5 9H12C12 10.2426 13.0074 11.25 14.25 11.25V9.75ZM13.5 9C13.5 8.58579 13.8358 8.25 14.25 8.25V6.75C13.0074 6.75 12 7.75736 12 9H13.5ZM14.25 8.25C14.6642 8.25 15 8.58579 15 9H16.5C16.5 7.75736 15.4926 6.75 14.25 6.75V8.25Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
@ -12,8 +12,10 @@ export * from "./edit";
|
|||||||
export * from "./enter";
|
export * from "./enter";
|
||||||
export * from "./eyeOff";
|
export * from "./eyeOff";
|
||||||
export * from "./eyeOn";
|
export * from "./eyeOn";
|
||||||
|
export * from "./feed";
|
||||||
export * from "./heartbeat";
|
export * from "./heartbeat";
|
||||||
export * from "./hide";
|
export * from "./hide";
|
||||||
|
export * from "./image";
|
||||||
export * from "./like";
|
export * from "./like";
|
||||||
export * from "./lume";
|
export * from "./lume";
|
||||||
export * from "./media";
|
export * from "./media";
|
||||||
|
Loading…
Reference in New Issue
Block a user