Compare commits
10 Commits
9aa85d1d2a
...
1b88677e91
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b88677e91 | ||
|
|
204c5481be | ||
|
|
ecf584323e | ||
|
|
ab3c60532e | ||
|
|
e5ecc66233 | ||
|
|
08cd69e3b9 | ||
|
|
77062163e8 | ||
|
|
1f37e05f50 | ||
|
|
9b18319486 | ||
|
|
b0e82f82c9 |
|
|
@ -3,7 +3,7 @@ import type { CommentAction } from "./schema";
|
||||||
type ActionAttachmentType = "empty" | "image" | "voice";
|
type ActionAttachmentType = "empty" | "image" | "voice";
|
||||||
|
|
||||||
const imageExtensions = ["jpeg", "jpg", "png"];
|
const imageExtensions = ["jpeg", "jpg", "png"];
|
||||||
const voiceExtensions = ["mp3"];
|
const voiceExtensions = ["mp3", "ogg"];
|
||||||
|
|
||||||
export function getActionAttachmentType(
|
export function getActionAttachmentType(
|
||||||
action: CommentAction,
|
action: CommentAction,
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,10 @@ import DialogActions from "@mui/material/DialogActions";
|
||||||
import DialogContent from "@mui/material/DialogContent";
|
import DialogContent from "@mui/material/DialogContent";
|
||||||
import DialogTitle from "@mui/material/DialogTitle";
|
import DialogTitle from "@mui/material/DialogTitle";
|
||||||
import { TextField } from "@mui/material";
|
import { TextField } from "@mui/material";
|
||||||
import { Button } from "@mui/material";
|
import { IconButton, Button } from "@mui/material";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
|
||||||
interface AddChainButtonProps {
|
export default function AddChainButton() {
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AddChainButton({ children }: AddChainButtonProps) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
|
|
@ -50,13 +47,14 @@ export default function AddChainButton({ children }: AddChainButtonProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<IconButton
|
||||||
|
size="medium"
|
||||||
|
color="info"
|
||||||
|
sx={{ borderRadius: "4px" }}
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
variant="outlined"
|
|
||||||
sx={{ width: "100%", fontWeight: "semibold" }}
|
|
||||||
>
|
>
|
||||||
{children}
|
<AddIcon />
|
||||||
</Button>
|
</IconButton>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export default function ChangeWaitForButton({
|
||||||
cleanWaitFor = waitFor;
|
cleanWaitFor = waitFor;
|
||||||
}
|
}
|
||||||
|
|
||||||
action.waitFor = cleanWaitFor > 10 ? 10 * 60 : cleanWaitFor * 60;
|
action.waitFor = cleanWaitFor * 60;
|
||||||
updateAction(chainId, actionIndex, action);
|
updateAction(chainId, actionIndex, action);
|
||||||
|
|
||||||
const chain = getChain(chainId)!;
|
const chain = getChain(chainId)!;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export default function LinkItem({
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-2 w-2 rounded-full bg-neutral",
|
"mr-2 max-h-2 min-h-2 min-w-2 max-w-2 rounded-full bg-neutral",
|
||||||
isSelected && "bg-white",
|
isSelected && "bg-white",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,15 @@ export default function Root({ children, className }: RootProps) {
|
||||||
return (
|
return (
|
||||||
<motion.ul layout className={cn("", className)}>
|
<motion.ul layout className={cn("", className)}>
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
{Children.map(children, (child, index) => (
|
{Children.map(children, (child) => (
|
||||||
<motion.li
|
<motion.li
|
||||||
layout
|
|
||||||
className="block"
|
className="block"
|
||||||
key={index}
|
// @ts-expect-error lazy... sry
|
||||||
initial={{ opacity: 0, height: 0 }}
|
key={child.key}
|
||||||
animate={{ opacity: 1, height: "auto" }}
|
layout
|
||||||
exit={{ opacity: 0, height: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
>
|
>
|
||||||
{child}
|
{child}
|
||||||
</motion.li>
|
</motion.li>
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,14 @@ export default function ActionCard({
|
||||||
className={`w-full select-none ${!action.text && "rounded-b-md"} rounded-t-md`}
|
className={`w-full select-none ${!action.text && "rounded-b-md"} rounded-t-md`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{attachmentType === "voice" && (
|
||||||
|
<div className="flex justify-center p-3">
|
||||||
|
<audio controls>
|
||||||
|
<source src={action.fileUrls[0]} type="audio/mp3" />
|
||||||
|
<source src={action.fileUrls[0]} type="audio/ogg" />
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{action.text && <div className="p-4">{action.text}</div>}
|
{action.text && <div className="p-4">{action.text}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ import CircularProgress from "@mui/material/CircularProgress";
|
||||||
|
|
||||||
import TextFieldsIcon from "@mui/icons-material/TextFields";
|
import TextFieldsIcon from "@mui/icons-material/TextFields";
|
||||||
import ImageIcon from "@mui/icons-material/Image";
|
import ImageIcon from "@mui/icons-material/Image";
|
||||||
// import RecordVoiceOverIcon from "@mui/icons-material/RecordVoiceOver";
|
import RecordVoiceOverIcon from "@mui/icons-material/RecordVoiceOver";
|
||||||
|
import MicNoneIcon from "@mui/icons-material/MicNone";
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
|
@ -59,6 +60,8 @@ export default function ActionEditor({
|
||||||
canExit = false,
|
canExit = false,
|
||||||
}: ActionEditorProps) {
|
}: ActionEditorProps) {
|
||||||
const imageRef = useRef<HTMLInputElement>(null);
|
const imageRef = useRef<HTMLInputElement>(null);
|
||||||
|
const voiceRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const attachmentType = initialAction
|
const attachmentType = initialAction
|
||||||
? getActionAttachmentType(initialAction)
|
? getActionAttachmentType(initialAction)
|
||||||
: "empty";
|
: "empty";
|
||||||
|
|
@ -75,6 +78,12 @@ export default function ActionEditor({
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [voiceUrl, setVoiceUrl] = useState<string | null>(
|
||||||
|
initialAction && attachmentType === "voice"
|
||||||
|
? initialAction.fileUrls[0]!
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
|
||||||
const [action, setAction] = useState<CommentAction>(
|
const [action, setAction] = useState<CommentAction>(
|
||||||
initialAction ?? {
|
initialAction ?? {
|
||||||
actionType: "comment",
|
actionType: "comment",
|
||||||
|
|
@ -106,6 +115,8 @@ export default function ActionEditor({
|
||||||
|
|
||||||
if (origin === "image") {
|
if (origin === "image") {
|
||||||
setImageUrl(fileUrl);
|
setImageUrl(fileUrl);
|
||||||
|
} else if (origin === "voice") {
|
||||||
|
setVoiceUrl(fileUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -137,8 +148,16 @@ export default function ActionEditor({
|
||||||
fileUrls: [imageUrl],
|
fileUrls: [imageUrl],
|
||||||
});
|
});
|
||||||
} else if (value === 2) {
|
} else if (value === 2) {
|
||||||
alert("Unsupported");
|
if (!voiceUrl) {
|
||||||
return;
|
alert("Загрузите голосовое :3");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave(actionIndex, {
|
||||||
|
actionType: action.actionType,
|
||||||
|
text: null,
|
||||||
|
fileUrls: [voiceUrl],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,11 +173,11 @@ export default function ActionEditor({
|
||||||
>
|
>
|
||||||
<Tab label="Текст" icon={<TextFieldsIcon />} {...a11yProps(0)} />
|
<Tab label="Текст" icon={<TextFieldsIcon />} {...a11yProps(0)} />
|
||||||
<Tab label="Изображение" icon={<ImageIcon />} {...a11yProps(1)} />
|
<Tab label="Изображение" icon={<ImageIcon />} {...a11yProps(1)} />
|
||||||
{/* <Tab
|
<Tab
|
||||||
label="Голосовое"
|
label="Голосовое"
|
||||||
icon={<RecordVoiceOverIcon />}
|
icon={<RecordVoiceOverIcon />}
|
||||||
{...a11yProps(2)}
|
{...a11yProps(2)}
|
||||||
/> */}
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
@ -212,6 +231,7 @@ export default function ActionEditor({
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref={imageRef}
|
ref={imageRef}
|
||||||
|
accept="image/*"
|
||||||
type="file"
|
type="file"
|
||||||
className="mb-4 hidden select-none"
|
className="mb-4 hidden select-none"
|
||||||
onChange={async (e) => await handleFileChange(e, "image")}
|
onChange={async (e) => await handleFileChange(e, "image")}
|
||||||
|
|
@ -233,9 +253,38 @@ export default function ActionEditor({
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
{/* <TabPanel value={value} index={2}>
|
<TabPanel value={value} index={2}>
|
||||||
In progress
|
<div className="mt-5 text-center">
|
||||||
</TabPanel> */}
|
<input
|
||||||
|
ref={voiceRef}
|
||||||
|
type="file"
|
||||||
|
accept=".mp3, .ogg"
|
||||||
|
className="mb-4 hidden select-none"
|
||||||
|
onChange={async (e) => await handleFileChange(e, "voice")}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
startIcon={<MicNoneIcon />}
|
||||||
|
variant="contained"
|
||||||
|
color="info"
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => {
|
||||||
|
if (!voiceRef.current) return;
|
||||||
|
voiceRef.current.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{voiceUrl ? "Изменить голосовое" : "Загрузить голосовое"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{voiceUrl && (
|
||||||
|
<div className="mt-10 flex justify-center">
|
||||||
|
<audio controls>
|
||||||
|
<source src={voiceUrl} type="audio/mp3" />
|
||||||
|
<source src={voiceUrl} type="audio/ogg" />
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,56 @@
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useState } from "react";
|
||||||
import { useParams } from "@tanstack/react-router";
|
import { useParams } from "@tanstack/react-router";
|
||||||
import { useChainState } from "@/entities/chain/model";
|
import { useChainState } from "@/entities/chain/model";
|
||||||
|
|
||||||
import List from "@/shared/ui/List";
|
import List from "@/shared/ui/List";
|
||||||
|
import { TextField } from "@mui/material";
|
||||||
import { AddChainButton } from "@/features/add-chain";
|
import { AddChainButton } from "@/features/add-chain";
|
||||||
|
|
||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const params = useParams({ strict: false });
|
const params = useParams({ strict: false });
|
||||||
const chains = useChainState((state) => state.chains || []);
|
const chains = useChainState((state) => state.chains || []);
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const filteredChains = useMemo(
|
||||||
|
() =>
|
||||||
|
chains.filter((value) =>
|
||||||
|
value.name?.toLowerCase().includes(query.toLocaleLowerCase()),
|
||||||
|
),
|
||||||
|
[query, chains],
|
||||||
|
);
|
||||||
|
|
||||||
// @ts-expect-error because i don't know why
|
// @ts-expect-error because i don't know why
|
||||||
const chainId: string | undefined = params.chainId;
|
const chainId: string | undefined = params.chainId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="no-scrollbar h-screen w-[300px] overflow-y-scroll bg-bg-color px-4 pb-8">
|
<div className="no-scrollbar relative flex h-screen w-[450px] flex-col bg-bg-color px-4 pb-2">
|
||||||
<List.Root className="mt-8 space-y-2">
|
<div className="mt-3 flex w-full items-start gap-x-3 pb-3">
|
||||||
{chains.map((chain, index) => (
|
<TextField
|
||||||
<List.LinkItem
|
fullWidth
|
||||||
key={index}
|
placeholder="Искать по названию"
|
||||||
to={`/$namespace/${chain._id}`}
|
color="primary"
|
||||||
isSelected={chainId == chain._id}
|
sx={{ backgroundColor: "#23253B", input: { color: "white" } }}
|
||||||
>
|
variant="outlined"
|
||||||
{chain.name}
|
size="small"
|
||||||
</List.LinkItem>
|
value={query}
|
||||||
))}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
</List.Root>
|
/>
|
||||||
|
<AddChainButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="no-scrollbar grow overflow-y-scroll">
|
||||||
<AddChainButton>
|
<List.Root className="space-y-2">
|
||||||
<span className="mr-2 text-2xl font-light">+</span>Добавить
|
{filteredChains.map((chain) => (
|
||||||
</AddChainButton>
|
<List.LinkItem
|
||||||
|
key={chain._id}
|
||||||
|
to={`/$namespace/${chain._id}`}
|
||||||
|
isSelected={chainId == chain._id}
|
||||||
|
>
|
||||||
|
{chain.name}
|
||||||
|
</List.LinkItem>
|
||||||
|
))}
|
||||||
|
</List.Root>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user