feat: add forward msg
This commit is contained in:
@@ -34,7 +34,6 @@ export default function ChatsSection({ selected, onSelect }: Props) {
|
||||
useEffect(() => {
|
||||
// Refetch chat list when returning to main screen
|
||||
if (!selected) {
|
||||
console.log("test");
|
||||
refetch();
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
+117
-54
@@ -7,7 +7,9 @@ import {
|
||||
CheckCheckIcon,
|
||||
CheckIcon,
|
||||
ChevronDown,
|
||||
CornerUpRightIcon,
|
||||
Loader2Icon,
|
||||
MousePointer2Icon,
|
||||
PaperclipIcon,
|
||||
PhoneCallIcon,
|
||||
SendIcon,
|
||||
@@ -32,9 +34,11 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { forwardDlg } from "./stores";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
@@ -53,6 +57,7 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
const { data: chats } = useChats();
|
||||
const { data: contacts } = useContacts();
|
||||
const [search, setSearch] = useState("");
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string> | null>(null);
|
||||
|
||||
const {
|
||||
data: messages,
|
||||
@@ -83,7 +88,10 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (chatId) msgContainerRef.current.scrollTop = 0;
|
||||
if (chatId) {
|
||||
msgContainerRef.current.scrollTop = 0;
|
||||
setSelectedIds(null);
|
||||
}
|
||||
}, [chatId]);
|
||||
|
||||
useMessageEvent((data) => {
|
||||
@@ -163,11 +171,23 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
});
|
||||
}, [messages, contacts, search]);
|
||||
|
||||
async function onDeleteMsg(id: string) {
|
||||
function onSelectMsg(id: string) {
|
||||
setSelectedIds((s) => {
|
||||
const set = new Set(s || []);
|
||||
if (!set.has(id)) {
|
||||
set.add(id);
|
||||
} else {
|
||||
set.delete(id);
|
||||
}
|
||||
return !set.size ? null : set;
|
||||
});
|
||||
}
|
||||
|
||||
async function onDeleteMsg(id: string | string[]) {
|
||||
if (!window.confirm("Delete this message?")) return;
|
||||
try {
|
||||
await client.exec(
|
||||
`return WPP.chat.deleteMessage('${chatId}', '${id}', true, true);`,
|
||||
`return WPP.chat.deleteMessage('${chatId}', ${JSON.stringify(id)}, true, true);`,
|
||||
);
|
||||
refetch();
|
||||
} catch (err) {
|
||||
@@ -192,63 +212,82 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
className={`flex min-w-0 h-full flex-1 flex-col bg-muted ${chatId ? "flex" : "hidden md:flex"}`}
|
||||
>
|
||||
<header className="flex items-center justify-between border-b border-border bg-card px-4 py-3 sm:px-6">
|
||||
<div className="flex min-w-0 items-center gap-3 w-full">
|
||||
<button
|
||||
aria-label="Back to conversations"
|
||||
onClick={() => router.goBack()}
|
||||
className="grid size-9 place-items-center rounded-full text-muted-foreground hover:bg-muted md:hidden cursor-pointer -ml-2"
|
||||
>
|
||||
<ArrowLeftIcon size={19} />
|
||||
</button>
|
||||
{selectedIds?.size ? (
|
||||
<div className="flex items-center gap-3 w-full">
|
||||
<button
|
||||
aria-label="Cancel selection"
|
||||
onClick={() => setSelectedIds(null)}
|
||||
className="grid size-10 place-items-center rounded-full text-muted-foreground hover:bg-muted cursor-pointer -ml-2"
|
||||
>
|
||||
<XIcon size={19} />
|
||||
</button>
|
||||
<p className="text-sm font-medium truncate">{`${selectedIds.size} selected`}</p>
|
||||
<div className="flex-1" />
|
||||
|
||||
<Avatar
|
||||
name={chat?.formattedTitle || chat?.name}
|
||||
className="size-10"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-lg"
|
||||
className="rounded-full"
|
||||
title="Forward"
|
||||
onClick={() => {
|
||||
forwardDlg.onOpen({ messageIds: [...selectedIds] });
|
||||
setSelectedIds(null);
|
||||
}}
|
||||
>
|
||||
<CornerUpRightIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-lg"
|
||||
className="rounded-full"
|
||||
title="Delete"
|
||||
onClick={() => {
|
||||
onDeleteMsg([...selectedIds]);
|
||||
setSelectedIds(null);
|
||||
}}
|
||||
>
|
||||
<Trash2Icon />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-w-0 items-center gap-3 w-full">
|
||||
<button
|
||||
aria-label="Back to conversations"
|
||||
onClick={() => router.goBack()}
|
||||
className="grid size-9 place-items-center rounded-full text-muted-foreground hover:bg-muted md:hidden cursor-pointer -ml-2"
|
||||
>
|
||||
<ArrowLeftIcon size={19} />
|
||||
</button>
|
||||
|
||||
<div className="min-w-0 flex-1 truncate">
|
||||
<h2 className="truncate text-sm font-semibold">
|
||||
{chat?.formattedTitle || chat?.name}
|
||||
</h2>
|
||||
<Avatar
|
||||
name={chat?.formattedTitle || chat?.name}
|
||||
className="size-10 -ml-2"
|
||||
/>
|
||||
|
||||
{/* <p className="text-xs text-[#1ea66a]">
|
||||
<div className="min-w-0 flex-1 truncate">
|
||||
<h2 className="truncate text-sm font-semibold">
|
||||
{chat?.formattedTitle || chat?.name}
|
||||
</h2>
|
||||
|
||||
{/* <p className="text-xs text-[#1ea66a]">
|
||||
{selected.online ? "Online now" : "Active recently"}
|
||||
</p> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchInput
|
||||
className="w-full max-w-40 md:max-w-sm"
|
||||
inputClassName="text-xs placeholder:text-xs"
|
||||
placeholder="Search..."
|
||||
defaultValue={search}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
</div>
|
||||
{/* <div className="flex items-center gap-1 text-muted-foreground">
|
||||
<button
|
||||
aria-label="Start video call"
|
||||
className="grid size-9 place-items-center rounded-full hover:bg-muted"
|
||||
>
|
||||
<VideoIcon size={18} />
|
||||
</button>
|
||||
<button
|
||||
aria-label="Start voice call"
|
||||
className="grid size-9 place-items-center rounded-full hover:bg-muted"
|
||||
>
|
||||
<Phone size={18} />
|
||||
</button>
|
||||
<button
|
||||
aria-label="Chat details"
|
||||
className="grid size-9 place-items-center rounded-full hover:bg-muted"
|
||||
>
|
||||
<EllipsisVertical size={18} />
|
||||
</button>
|
||||
</div> */}
|
||||
<SearchInput
|
||||
className="w-full max-w-40 md:max-w-sm"
|
||||
inputClassName="text-xs placeholder:text-xs"
|
||||
placeholder="Search..."
|
||||
defaultValue={search}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div
|
||||
ref={msgContainerRef}
|
||||
className="message-pattern flex-1 px-4 py-6 sm:px-8 flex flex-col-reverse overflow-y-auto"
|
||||
className="message-pattern flex-1 py-6 flex flex-col-reverse overflow-y-auto"
|
||||
>
|
||||
{/* <div className="mb-6 flex items-center justify-center gap-2 text-[11px] text-muted-foreground">
|
||||
<span className="h-px flex-1 bg-border" />
|
||||
@@ -261,9 +300,20 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
id={item.id.replace(/^(?:false_|true_)/, "")}
|
||||
key={`${item.timestamp}-${index}`}
|
||||
className={cn(
|
||||
"mb-3 flex",
|
||||
"flex px-4 sm:px-8 py-1.5",
|
||||
item.fromMe ? "justify-end" : "justify-start",
|
||||
selectedIds != null && "cursor-pointer hover:bg-[#26855c]/10",
|
||||
selectedIds?.has(item.id) && "bg-[#26855c]/30!",
|
||||
)}
|
||||
role={selectedIds != null ? "button" : undefined}
|
||||
onClick={
|
||||
selectedIds != null
|
||||
? (e) => {
|
||||
if (e.target !== e.currentTarget) return;
|
||||
onSelectMsg(item.id);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="max-w-[82%] sm:max-w-[65%]">
|
||||
<a
|
||||
@@ -280,7 +330,7 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
</a>
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-2xl px-4 py-2.5 text-sm shadow-sm relative group",
|
||||
"rounded-2xl px-4 py-2.5 text-sm shadow-sm relative group rounded-tr-md",
|
||||
item.fromMe
|
||||
? "rounded-br-md bg-[#d8f5e8] dark:bg-[#26855c] text-foreground"
|
||||
: "rounded-bl-md bg-card text-card-foreground",
|
||||
@@ -290,15 +340,28 @@ export default function MessagesSection({ chatId, maxCount = 20 }: Props) {
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="default"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="absolute top-1 right-1 z-1 rounded-full opacity-0 group-hover:opacity-100"
|
||||
className="absolute top-0 right-0 z-1 md:opacity-0 group-hover:opacity-100"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ChevronDown />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => onSelectMsg(item.id)}>
|
||||
<MousePointer2Icon />
|
||||
Select
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
forwardDlg.onOpen({ messageIds: [item.id] })
|
||||
}
|
||||
>
|
||||
<CornerUpRightIcon />
|
||||
Forward
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onDeleteMsg(item.id)}>
|
||||
<Trash2Icon />
|
||||
Delete
|
||||
|
||||
@@ -3,6 +3,7 @@ import DevicesSection from "./devices";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
import { useHashRouter } from "@/hooks/useHashRouter";
|
||||
import MessagesSection from "./messages";
|
||||
import ForwardDialog from "@/components/forward-dialog";
|
||||
|
||||
export default function Page() {
|
||||
const router = useHashRouter();
|
||||
@@ -75,6 +76,8 @@ export default function Page() {
|
||||
|
||||
{chatId ? <MessagesSection chatId={chatId} /> : null}
|
||||
{/* </div> */}
|
||||
|
||||
<ForwardDialog />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createDisclosure } from "@/lib/utils";
|
||||
|
||||
export const forwardDlg = createDisclosure<{
|
||||
messageIds: string[];
|
||||
}>();
|
||||
@@ -0,0 +1,100 @@
|
||||
import { forwardDlg } from "@/app/stores";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "./ui/button";
|
||||
import { useChats } from "@/hooks/useChats";
|
||||
import Select, { type Option } from "@/components/ui/react-select";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useWAClient } from "@/app/context";
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
|
||||
export default function ForwardDialog() {
|
||||
const { data, isOpen } = forwardDlg.useStore();
|
||||
const { client } = useWAClient();
|
||||
const { data: chats } = useChats();
|
||||
|
||||
const [selectedChat, setSelectedChat] = useState<Option[]>([]);
|
||||
const fwd = useMutation({
|
||||
mutationFn: async (payload: { chatIds: string[]; msgIds: string[] }) => {
|
||||
const { chatIds, msgIds } = payload;
|
||||
for (const chatId of chatIds) {
|
||||
await client.exec(
|
||||
`return WPP.chat.forwardMessages('${chatId}', ${JSON.stringify(msgIds)});`,
|
||||
);
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
alert("Message forwarded!");
|
||||
forwardDlg.onClose();
|
||||
},
|
||||
onError(err) {
|
||||
alert(err.message);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) setSelectedChat([]);
|
||||
}, [isOpen]);
|
||||
|
||||
const chatOptions = useMemo(() => {
|
||||
return (chats || []).map((chat) => ({
|
||||
value: chat.id,
|
||||
label: chat.formattedTitle || chat.name,
|
||||
}));
|
||||
}, [chats]);
|
||||
|
||||
function onSubmit() {
|
||||
if (
|
||||
fwd.isPending ||
|
||||
!data?.messageIds?.length ||
|
||||
!window.confirm("Send message?")
|
||||
)
|
||||
return;
|
||||
|
||||
fwd.mutate({
|
||||
chatIds: selectedChat.map((i) => i.value),
|
||||
msgIds: data!.messageIds,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={forwardDlg.onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Forward Messages</DialogTitle>
|
||||
<DialogDescription>
|
||||
{data?.messageIds.length} messages
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div>
|
||||
<Select
|
||||
options={chatOptions}
|
||||
isMulti
|
||||
value={selectedChat}
|
||||
onChange={(i) => setSelectedChat(i as never)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose render={<Button variant="outline">Cancel</Button>} />
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
disabled={!selectedChat.length || fwd.isPending}
|
||||
>
|
||||
{fwd.isPending && <Loader2Icon className="mr-2 animate-spin" />}
|
||||
Send Message
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import * as React from "react";
|
||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
||||
import { cn } from "cn";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Backdrop.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Backdrop
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: DialogPrimitive.Popup.Props & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Popup
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-6 rounded-xl bg-popover p-6 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-md data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-4 right-4"
|
||||
size="icon-sm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Popup>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
||||
Close
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("leading-none font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Description.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
import BaseSelect, {
|
||||
components,
|
||||
type ControlProps,
|
||||
type MultiValueProps,
|
||||
type OptionProps,
|
||||
type SingleValueProps,
|
||||
type StylesConfig,
|
||||
} from "react-select";
|
||||
|
||||
export type Option = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Custom Control
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
function CustomControl(props: ControlProps<Option, true>) {
|
||||
return <components.Control {...props}>{props.children}</components.Control>;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Custom Option
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
function CustomOption(props: OptionProps<Option, true>) {
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{props.data.label}</span>
|
||||
|
||||
{props.isSelected && <span className="text-zinc-300">✓</span>}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Custom Single Value
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
function CustomSingleValue(props: SingleValueProps<Option>) {
|
||||
return (
|
||||
<components.SingleValue {...props}>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-2 w-2 rounded-full bg-indigo-400" />
|
||||
{props.data.label}
|
||||
</div>
|
||||
</components.SingleValue>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Custom Multi Value
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
function CustomMultiValue(props: MultiValueProps<Option, true>) {
|
||||
return (
|
||||
<components.MultiValue {...props}>
|
||||
<span>{props.data.label}</span>
|
||||
</components.MultiValue>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Shadcn-inspired dark styles
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
const darkStyles: StylesConfig<Option, true> = {
|
||||
control: (base, state) => ({
|
||||
...base,
|
||||
|
||||
minHeight: 40,
|
||||
backgroundColor: "#09090b",
|
||||
borderColor: state.isFocused ? "#71717a" : "#27272a",
|
||||
borderRadius: 6,
|
||||
|
||||
boxShadow: state.isFocused ? "0 0 0 1px #71717a" : "none",
|
||||
|
||||
cursor: "pointer",
|
||||
|
||||
"&:hover": {
|
||||
borderColor: "#3f3f46",
|
||||
},
|
||||
}),
|
||||
|
||||
valueContainer: (base) => ({
|
||||
...base,
|
||||
padding: "3px 8px",
|
||||
gap: 4,
|
||||
}),
|
||||
|
||||
input: (base) => ({
|
||||
...base,
|
||||
color: "#fafafa",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
}),
|
||||
|
||||
placeholder: (base) => ({
|
||||
...base,
|
||||
color: "#71717a",
|
||||
}),
|
||||
|
||||
singleValue: (base) => ({
|
||||
...base,
|
||||
color: "#fafafa",
|
||||
}),
|
||||
|
||||
// ───────────────────────────────────────────
|
||||
// Menu
|
||||
// ───────────────────────────────────────────
|
||||
|
||||
menu: (base) => ({
|
||||
...base,
|
||||
marginTop: 4,
|
||||
backgroundColor: "#09090b",
|
||||
border: "1px solid #27272a",
|
||||
borderRadius: 6,
|
||||
overflow: "hidden",
|
||||
|
||||
boxShadow:
|
||||
"0 10px 15px -3px rgb(0 0 0 / 0.3), " + "0 4px 6px -4px rgb(0 0 0 / 0.3)",
|
||||
}),
|
||||
|
||||
menuList: (base) => ({
|
||||
...base,
|
||||
padding: 4,
|
||||
}),
|
||||
|
||||
// ───────────────────────────────────────────
|
||||
// Options
|
||||
// ───────────────────────────────────────────
|
||||
|
||||
option: (base, state) => ({
|
||||
...base,
|
||||
|
||||
borderRadius: 4,
|
||||
padding: "7px 8px",
|
||||
|
||||
backgroundColor: state.isSelected
|
||||
? "#27272a"
|
||||
: state.isFocused
|
||||
? "#18181b"
|
||||
: "transparent",
|
||||
|
||||
color: state.isDisabled ? "#52525b" : "#fafafa",
|
||||
|
||||
cursor: state.isDisabled ? "not-allowed" : "pointer",
|
||||
|
||||
"&:active": {
|
||||
backgroundColor: "#27272a",
|
||||
},
|
||||
}),
|
||||
|
||||
// ───────────────────────────────────────────
|
||||
// Multi value
|
||||
// ───────────────────────────────────────────
|
||||
|
||||
multiValue: (base) => ({
|
||||
...base,
|
||||
|
||||
backgroundColor: "#27272a",
|
||||
borderRadius: 4,
|
||||
}),
|
||||
|
||||
multiValueLabel: (base) => ({
|
||||
...base,
|
||||
|
||||
color: "#e4e4e7",
|
||||
padding: "2px 6px",
|
||||
}),
|
||||
|
||||
multiValueRemove: (base) => ({
|
||||
...base,
|
||||
|
||||
color: "#a1a1aa",
|
||||
borderRadius: "0 4px 4px 0",
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: "#3f3f46",
|
||||
color: "#fafafa",
|
||||
},
|
||||
}),
|
||||
|
||||
// ───────────────────────────────────────────
|
||||
// Indicators
|
||||
// ───────────────────────────────────────────
|
||||
|
||||
indicatorSeparator: () => ({
|
||||
display: "none",
|
||||
}),
|
||||
|
||||
dropdownIndicator: (base) => ({
|
||||
...base,
|
||||
|
||||
color: "#71717a",
|
||||
padding: "0 8px",
|
||||
|
||||
"&:hover": {
|
||||
color: "#a1a1aa",
|
||||
},
|
||||
}),
|
||||
|
||||
clearIndicator: (base) => ({
|
||||
...base,
|
||||
|
||||
color: "#71717a",
|
||||
|
||||
"&:hover": {
|
||||
color: "#fafafa",
|
||||
},
|
||||
}),
|
||||
|
||||
// ───────────────────────────────────────────
|
||||
// Messages
|
||||
// ───────────────────────────────────────────
|
||||
|
||||
noOptionsMessage: (base) => ({
|
||||
...base,
|
||||
color: "#71717a",
|
||||
}),
|
||||
|
||||
loadingMessage: (base) => ({
|
||||
...base,
|
||||
color: "#71717a",
|
||||
}),
|
||||
};
|
||||
// ─────────────────────────────────────────────
|
||||
// Select
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
type Props = React.ComponentProps<typeof BaseSelect>;
|
||||
|
||||
function Select({ ...props }: Props) {
|
||||
return (
|
||||
<BaseSelect
|
||||
styles={darkStyles}
|
||||
components={{
|
||||
Control: CustomControl,
|
||||
Option: CustomOption,
|
||||
SingleValue: CustomSingleValue,
|
||||
MultiValue: CustomMultiValue,
|
||||
}}
|
||||
{...(props as any)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Select;
|
||||
+15
-1
@@ -1,5 +1,5 @@
|
||||
import type { ContactDTO, MessageIdDTO, SerializedId } from "@shared/dto";
|
||||
|
||||
import { createStore, useStore } from "zustand";
|
||||
export { cn } from "cn";
|
||||
|
||||
export function getId(id?: string | SerializedId | MessageIdDTO | null) {
|
||||
@@ -45,3 +45,17 @@ export async function getFilesAsBase64(files: File[]) {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function createDisclosure<T = any>(data?: T | null) {
|
||||
const store = createStore(() => ({
|
||||
isOpen: false,
|
||||
data,
|
||||
}));
|
||||
|
||||
return Object.assign(store, {
|
||||
onOpen: (data?: T | null) => store.setState({ isOpen: true, data }),
|
||||
onClose: () => store.setState({ isOpen: false }),
|
||||
onOpenChange: (open: boolean) => store.setState({ isOpen: open }),
|
||||
useStore: () => useStore(store),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user