refactor: more tailwind
This commit is contained in:
@@ -34,7 +34,7 @@ const Avatar: FC<Props> = ({
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
className="rounded-full flex-center"
|
||||
className={`rounded-full flex-center ${rest.className || ""}`}
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
|
||||
@@ -1,69 +1,8 @@
|
||||
import { ChangeEvent, FC, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import Avatar from "./Avatar";
|
||||
import uploadIcon from "../../assets/icons/upload.image.svg?url";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledWrapper = styled.div<{ size: number }>`
|
||||
width: ${({ size }) => `${size}px`};
|
||||
height: ${({ size }) => `${size}px`};
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
.avatar {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: #eee;
|
||||
img {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
input[type="file"] {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.tip {
|
||||
white-space: nowrap;
|
||||
padding: 4px;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
&:hover .tip {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
display: none;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
&:hover .icon {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
import clsx from "clsx";
|
||||
|
||||
type UID = number;
|
||||
interface Props {
|
||||
@@ -102,16 +41,18 @@ const AvatarUploader: FC<Props> = ({
|
||||
setUploading(false);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<StyledWrapper size={size} className={className}>
|
||||
<div className="avatar">
|
||||
<Avatar width={size} height={size} type={type} src={url} name={name} className={className} />
|
||||
<div style={{ width: `${size}px`, height: `${size}px` }} className={clsx(className, "relative group")}>
|
||||
<div className="group overflow-hidden relative w-full h-full rounded-full bg-gray-50">
|
||||
<Avatar width={size} height={size} type={type} src={url} name={name} className={`${className} object-cover w-full h-full`} />
|
||||
{!disabled && (
|
||||
<>
|
||||
<div className="tip">
|
||||
<div className="flex-center flex-col whitespace-nowrap hidden group-hover:flex p-1 absolute top-0 left-0 right-0 bottom-0 bg-black/50 text-white font-bold text-xs">
|
||||
{uploading ? t("status.uploading") : t("action.change_avatar")}
|
||||
</div>
|
||||
<input
|
||||
className="opacity-0 absolute top-0 left-0 right-0 bottom-0 block cursor-pointer"
|
||||
multiple={false}
|
||||
onChange={handleUpload}
|
||||
type="file"
|
||||
@@ -122,8 +63,8 @@ const AvatarUploader: FC<Props> = ({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{!disabled && <img src={uploadIcon} alt="icon" className="icon" />}
|
||||
</StyledWrapper>
|
||||
{!disabled && <img src={uploadIcon} alt="icon" className="hidden w-7 h-7 absolute top-0 right-0 group-hover:block" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
type?: "chat" | "user";
|
||||
}
|
||||
const classes = {
|
||||
box: "w-[200px] h-[200px] cursor-pointer bg-[#f9fafb] rounded-3xl flex-center flex-col gap-4",
|
||||
box: "w-[150px] md:w-[200px] h-[150px] md:h-[200px] cursor-pointer bg-[#f9fafb] rounded-3xl flex-center flex-col gap-4",
|
||||
boxIcon: "w-10 h-10",
|
||||
boxTip: "px-5 text-sm text-[#475467] font-bold text-center"
|
||||
};
|
||||
@@ -45,7 +45,7 @@ const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => {
|
||||
{t("desc")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 grid-rows-2 gap-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 grid-rows-2 gap-2 md:gap-6 mt-auto">
|
||||
<div className={classes.box} onClick={toggleInviteModalVisible}>
|
||||
<IconInvite className={classes.boxIcon} />
|
||||
<div className={classes.boxTip}>{t("invite")}</div>
|
||||
|
||||
@@ -1,50 +1,8 @@
|
||||
import { FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import Avatar from "./Avatar";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import clsx from "clsx";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
&.compact {
|
||||
padding: 0;
|
||||
}
|
||||
&.interactive {
|
||||
&:hover,
|
||||
&.active {
|
||||
background: rgba(116, 127, 141, 0.1);
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
width: ${({ size }: { size: number }) => `${size}px`};
|
||||
height: ${({ size }: { size: number }) => `${size}px`};
|
||||
position: relative;
|
||||
img {
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
.name {
|
||||
display: flex;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #52525b;
|
||||
.txt {
|
||||
max-width: 140px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
interactive?: boolean;
|
||||
@@ -63,12 +21,12 @@ const Channel: FC<Props> = ({ interactive = true, id, compact = false, avatarSiz
|
||||
|
||||
if (!channel) return null;
|
||||
const { name, members = [], is_public, icon } = channel;
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
size={avatarSize}
|
||||
className={`${interactive ? "interactive" : ""} ${compact ? "compact" : ""}`}
|
||||
<div
|
||||
className={clsx(`flex items-center justify-start gap-2 p-2 rounded-lg select-none`, compact && "p-0", interactive && "hover:bg-gray-500/10")}
|
||||
>
|
||||
<div className="avatar">
|
||||
<div className={`cursor-pointer relative`} style={{ width: `${avatarSize}px`, height: `${avatarSize}px` }}>
|
||||
<Avatar
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
@@ -76,14 +34,15 @@ const Channel: FC<Props> = ({ interactive = true, id, compact = false, avatarSiz
|
||||
src={icon}
|
||||
name={"#"}
|
||||
alt="avatar"
|
||||
className="!w-full !h-full rounded-full"
|
||||
/>
|
||||
</div>
|
||||
{!compact && (
|
||||
<div className="name">
|
||||
<span className="txt">{name}</span> ({is_public ? totalMemberCount : members.length})
|
||||
<div className="flex text-sm text-gray-500 font-semibold">
|
||||
<span className="max-w-[140px] whitespace-nowrap overflow-hidden text-ellipsis">{name}</span> ({is_public ? totalMemberCount : members.length})
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
import { useState, useEffect, FC } from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Styled = styled.div`
|
||||
background-color: #fff;
|
||||
height: 218px;
|
||||
padding: 15px 15px 0 15px;
|
||||
line-height: 1.4;
|
||||
overflow: scroll;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
url: string;
|
||||
@@ -28,7 +17,7 @@ const Doc: FC<Props> = ({ url }) => {
|
||||
}, [url]);
|
||||
if (!content) return null;
|
||||
|
||||
return <Styled>{content}</Styled>;
|
||||
return <div className="bg-white h-[218px] p-[15px] pb-0 whitespace-pre-wrap break-all">{content}</div>;
|
||||
};
|
||||
|
||||
export default Doc;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import Styled from "./styled";
|
||||
import ImageMessage from "./ImageMessage";
|
||||
import useRemoveLocalMessage from "../../hook/useRemoveLocalMessage";
|
||||
import useUploadFile from "../../hook/useUploadFile";
|
||||
@@ -12,6 +11,7 @@ import IconDownload from "../../../assets/icons/download.svg";
|
||||
import IconClose from "../../../assets/icons/close.circle.svg";
|
||||
import VideoMessage from "./VideoMessage";
|
||||
import AudioMessage from "./AudioMessage";
|
||||
import clsx from "clsx";
|
||||
|
||||
const isLocalFile = (content: string) => {
|
||||
return content.startsWith("blob:");
|
||||
@@ -117,7 +117,7 @@ const FileMessage: FC<Props> = ({
|
||||
removeLocalMessage(properties.local_id);
|
||||
};
|
||||
if (!properties) return null;
|
||||
const icon = getFileIcon(content_type, name);
|
||||
const icon = getFileIcon(content_type, name, "w-9 h-auto");
|
||||
|
||||
if (!content || !name) return null;
|
||||
|
||||
@@ -156,21 +156,21 @@ const FileMessage: FC<Props> = ({
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Styled className={`file_message ${sending ? "sending" : ""}`}>
|
||||
<div className="basic">
|
||||
<div className={clsx(`file_message bg-slate-50 border border-solid border-gray-300 box-border w-[370px] h-[66px] rounded-md`, sending && "opacity-90")}>
|
||||
<div className="p-2 flex items-center justify-between gap-2">
|
||||
{icon}
|
||||
<div className="info">
|
||||
<span className="name">{name}</span>
|
||||
<span className="details">
|
||||
<div className="flex flex-col gap-1 w-full overflow-hidden">
|
||||
<span className="font-semibold text-sm text-gray-800 whitespace-nowrap text-ellipsis">{name}</span>
|
||||
<span className="whitespace-nowrap text-xs text-gray-500 flex gap-4">
|
||||
{sending ? (
|
||||
<Progress value={progress} width={"80%"} />
|
||||
) : (
|
||||
<>
|
||||
<i className="size">{formatBytes(size)}</i>
|
||||
<i className="time">{dayjs(created_at).fromNow()}</i>
|
||||
<i>{formatBytes(size)}</i>
|
||||
<i>{dayjs(created_at).fromNow()}</i>
|
||||
{fromUser && (
|
||||
<i className="from">
|
||||
by <strong>{fromUser.name}</strong>
|
||||
<i>
|
||||
by <strong className="font-bold">{fromUser.name}</strong>
|
||||
</i>
|
||||
)}
|
||||
</>
|
||||
@@ -178,14 +178,14 @@ const FileMessage: FC<Props> = ({
|
||||
</span>
|
||||
</div>
|
||||
{sending ? (
|
||||
<IconClose className="cancel" onClick={handleCancel} />
|
||||
<IconClose className="cursor-pointer" onClick={handleCancel} />
|
||||
) : (
|
||||
<a className="download" download={name} href={`${content}&download=true`}>
|
||||
<IconDownload />
|
||||
<a className="whitespace-nowrap" download={name} href={`${content}&download=true`}>
|
||||
<IconDownload className="fill-gray-500" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</Styled>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const Styled = styled.div`
|
||||
background: #f3f4f6;
|
||||
border: 1px solid #d4d4d4;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6px;
|
||||
width: 370px;
|
||||
height: 66px;
|
||||
&.sending {
|
||||
opacity: 0.9;
|
||||
}
|
||||
* {
|
||||
user-select: text;
|
||||
}
|
||||
.basic {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
.icon {
|
||||
width: 36px;
|
||||
height: 48px;
|
||||
|
||||
}
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #1c1c1e;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.details {
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #616161;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
.from strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
.download {
|
||||
white-space: nowrap;
|
||||
svg path{
|
||||
fill: #616161;
|
||||
}
|
||||
}
|
||||
.cancel {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Styled;
|
||||
@@ -5,7 +5,6 @@ import Button from "../styled/Button";
|
||||
import Input from "../styled/Input";
|
||||
import Channel from "../Channel";
|
||||
import User from "../User";
|
||||
// import Channel from "../Channel";
|
||||
import Reply from "../Message/Reply";
|
||||
import StyledWrapper from "./styled";
|
||||
import useForwardMessage from "../../hook/useForwardMessage";
|
||||
|
||||
@@ -1,23 +1,8 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import IconGithub from "../../assets/icons/github.svg";
|
||||
import styled from "styled-components";
|
||||
import Button from "./styled/Button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledSocialButton = styled(Button)`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: #344054;
|
||||
border: 1px solid #d0d5dd;
|
||||
background: none !important;
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
type Props = {
|
||||
source?: "widget" | "webapp",
|
||||
@@ -60,10 +45,10 @@ const GithubLoginButton: FC<Props> = ({ type = "login", source = "webapp", clien
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledSocialButton onClick={handleGithubLogin}>
|
||||
<IconGithub className="icon" />
|
||||
<Button className="flex-center gap-3 ghost !text-gray-600 !border-[#d0d5dd]" onClick={handleGithubLogin}>
|
||||
<IconGithub className="w-6 h-6" />
|
||||
{` ${type === "login" ? t("login.github") : t("reg.github")}`}
|
||||
</StyledSocialButton>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
export default GithubLoginButton;
|
||||
|
||||
@@ -1,54 +1,12 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
import { GoogleLogin, GoogleOAuthProvider } from "@react-oauth/google";
|
||||
import toast from "react-hot-toast";
|
||||
import styled from "styled-components";
|
||||
import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
|
||||
import IconGoogle from "../../assets/icons/google.svg";
|
||||
import Button from "./styled/Button";
|
||||
import { useLoginMutation } from "../../app/services/auth";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledSocialButton = styled(Button)`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: white !important;
|
||||
border: 1px solid #d0d5dd;
|
||||
color: #344054;
|
||||
height: 46px;
|
||||
overflow: hidden;
|
||||
.mask {
|
||||
background: inherit;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
z-index: 998;
|
||||
height: 40px;
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
> .hide {
|
||||
left: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
iframe {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.hide {
|
||||
opacity: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
loadError?: boolean;
|
||||
@@ -86,16 +44,16 @@ const GoogleLoginInner: FC<Props> = ({ type = "login", loaded, loadError }) => {
|
||||
}, [error]);
|
||||
|
||||
return (
|
||||
<StyledSocialButton disabled={!loaded || isLoading}>
|
||||
<div className="mask">
|
||||
<IconGoogle className="icon" />
|
||||
<Button className="group relative w-full !bg-white !text-gray-600 !h-[46px] overflow-hidden !border !border-solid !border-[#d0d5dd]" disabled={!loaded || isLoading}>
|
||||
<div className="absolute left-0 top-0 w-full flex-center gap-3 z-[998] h-10 bg-inherit">
|
||||
<IconGoogle className="w-6 h-6" />
|
||||
{loadError
|
||||
? "Script Load Error!"
|
||||
: loaded
|
||||
? `${type === "login" ? t("login.google") : t("reg.google")}`
|
||||
: `Initializing`}
|
||||
</div>
|
||||
<div className="hide">
|
||||
<div className="absolute left-0 top-0 w-full group-hover:opacity-0 group-hover:z-[999]">
|
||||
<GoogleLogin
|
||||
width="360px"
|
||||
onSuccess={(res) => {
|
||||
@@ -107,7 +65,7 @@ const GoogleLoginInner: FC<Props> = ({ type = "login", loaded, loadError }) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</StyledSocialButton>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,51 +1,8 @@
|
||||
import { FC } from "react";
|
||||
import styled from "styled-components";
|
||||
import Modal from "../Modal";
|
||||
import IconClose from "../../../assets/icons/close.svg";
|
||||
import Button from "../styled/Button";
|
||||
|
||||
const Styled = styled.div`
|
||||
position: relative;
|
||||
margin-top: 15px;
|
||||
pointer-events: all;
|
||||
width: 406px;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
box-shadow: 0 25px 50px rgba(31, 41, 55, 0.25);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
.tip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
color: #344054;
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.desc {
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
.btns {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
}
|
||||
.close {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
handleInstall?: () => void;
|
||||
closePrompt?: () => void;
|
||||
@@ -54,13 +11,13 @@ interface Props {
|
||||
const Prompt: FC<Props> = ({ handleInstall, closePrompt }) => {
|
||||
return (
|
||||
<Modal mask={false}>
|
||||
<Styled>
|
||||
<IconClose className="close" onClick={closePrompt} />
|
||||
<div className="tip">
|
||||
<h2 className="title">Install web app on desktop?</h2>
|
||||
<p className="desc">Add to desktop for quick access to this app.</p>
|
||||
<div className="relative pointer-events-auto mt-4 w-[406px] p-4 rounded-md bg-white shadow-md flex flex-col gap-3">
|
||||
<IconClose className="absolute top-4 right-4 cursor-pointer" onClick={closePrompt} />
|
||||
<div className="flex flex-col gap-4 text-gray-600">
|
||||
<h2 className="font-semibold">Install web app on desktop?</h2>
|
||||
<p className="text-sm">Add to desktop for quick access to this app.</p>
|
||||
</div>
|
||||
<div className="btns">
|
||||
<div className="w-full flex justify-end gap-4">
|
||||
<Button className="ghost cancel small" onClick={closePrompt}>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -68,7 +25,7 @@ const Prompt: FC<Props> = ({ handleInstall, closePrompt }) => {
|
||||
Install
|
||||
</Button>
|
||||
</div>
|
||||
</Styled>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,21 +3,10 @@ import "prismjs/themes/prism.css";
|
||||
import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css";
|
||||
//@ts-ignore
|
||||
import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js";
|
||||
|
||||
import { Viewer } from "@toast-ui/react-editor";
|
||||
import styled from "styled-components";
|
||||
|
||||
import ImagePreviewModal, { PreviewImageData } from "./ImagePreviewModal";
|
||||
|
||||
const Styled = styled.div`
|
||||
* {
|
||||
user-select: text;
|
||||
}
|
||||
.toastui-editor-contents {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
`;
|
||||
|
||||
interface IProps {
|
||||
content: string;
|
||||
@@ -60,9 +49,9 @@ const MarkdownRender: FC<IProps> = ({ content }) => {
|
||||
{previewImage && (
|
||||
<ImagePreviewModal download={false} data={previewImage} closeModal={closePreviewModal} />
|
||||
)}
|
||||
<Styled ref={mdContainer}>
|
||||
<div ref={mdContainer} id="MARKDOWN_CONTAINER">
|
||||
<Viewer initialValue={content} plugins={[codeSyntaxHighlight]}></Viewer>
|
||||
</Styled>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,43 +1,18 @@
|
||||
// import React from 'react'
|
||||
import Tippy, { TippyProps } from "@tippyjs/react";
|
||||
import styled from "styled-components";
|
||||
import { FC } from "react";
|
||||
import Tippy, { TippyProps } from "@tippyjs/react";
|
||||
import clsx from "clsx";
|
||||
|
||||
const StyledTip = styled.div`
|
||||
position: relative;
|
||||
background: #fff;
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #1d2939;
|
||||
border-radius: var(--br);
|
||||
box-shadow: 0 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||
&::after {
|
||||
background-color: inherit;
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 1px;
|
||||
transform-origin: center;
|
||||
}
|
||||
&.right::after {
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translate3d(-50%, -50%, 0) rotate(45deg);
|
||||
}
|
||||
&.top::after {
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translate3d(-50%, 50%, 0) rotate(45deg);
|
||||
}
|
||||
&.bottom::after {
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, -50%, 0) rotate(45deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Triangle: FC<Pick<TippyProps, "placement">> = ({ placement }) => {
|
||||
if (placement == "left") return null;
|
||||
const cls = clsx("w-3 h-3 bg-inherit absolute rounded-[1px] origin-center rotate-45",
|
||||
placement == "right" && "left-0 top-1/2 -translate-x-1/2 -translate-y-1/2",
|
||||
placement == "top" && "left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2",
|
||||
placement == "bottom" && "top-0 left-1/2 -translate-x-1/2 -translate-y-1/2",
|
||||
|
||||
);
|
||||
return <i className={cls}></i>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
tip: string;
|
||||
@@ -52,7 +27,10 @@ const Tooltip: FC<Props> = ({ tip = "", placement = "right", delay = null, child
|
||||
duration={delay ? defaultDuration : 0}
|
||||
delay={delay ?? [150, 0]}
|
||||
placement={placement}
|
||||
content={<StyledTip className={placement}>{tip}</StyledTip>}
|
||||
content={<div className="relative bg-white px-3 py-2 text-xs rounded-lg drop-shadow text-gray-700">
|
||||
<Triangle placement={placement} />
|
||||
{tip}
|
||||
</div>}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -6,7 +6,6 @@ import IconBot from "../../../assets/icons/bot.svg";
|
||||
import Avatar from "../Avatar";
|
||||
import Profile from "../Profile";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import StyledWrapper from "./styled";
|
||||
import useContextMenu from "../../hook/useContextMenu";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import clsx from "clsx";
|
||||
@@ -44,6 +43,11 @@ const User: FC<Props> = ({
|
||||
};
|
||||
if (!curr) return null;
|
||||
const online = curr.online || curr.uid == loginUid;
|
||||
const containerClass = clsx(`relative flex items-center justify-start gap-2 p-2 rounded-lg select-none `, interactive && "hover:bg-gray-500/10", compact && "p-0");
|
||||
const nameClass = clsx(`text-sm text-gray-500 max-w-[190px] overflow-hidden text-ellipsis font-semibold`);
|
||||
const statusClass = clsx(`absolute -bottom-0.5 -right-1.5 w-3 h-3 box-content rounded-full border-[2px] border-solid border-white`,
|
||||
online ? "bg-[#22c55e]" : "bg-[#a1a1aa]",
|
||||
compact && "w-3.5 h-3.5");
|
||||
if (!popover)
|
||||
return (
|
||||
<ContextMenu
|
||||
@@ -53,30 +57,30 @@ const User: FC<Props> = ({
|
||||
visible={contextMenuVisible}
|
||||
hide={hideContextMenu}
|
||||
>
|
||||
<StyledWrapper
|
||||
size={avatarSize}
|
||||
className={`${interactive ? "interactive" : ""} ${compact ? "compact" : ""} relative`}
|
||||
<div
|
||||
className={containerClass}
|
||||
onDoubleClick={dm ? handleDoubleClick : undefined}
|
||||
onContextMenu={enableContextMenu ? handleContextMenuEvent : undefined}
|
||||
>
|
||||
<div className="avatar">
|
||||
<div className="cursor-pointer relative" style={{ width: `${avatarSize}px`, height: `${avatarSize}px` }}>
|
||||
<Avatar
|
||||
className="w-full h-full rounded-full object-cover"
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
src={curr.avatar}
|
||||
name={curr.name}
|
||||
alt="avatar"
|
||||
/>
|
||||
<div className={`status ${online ? "online" : "offline"}`}></div>
|
||||
<div className={statusClass}></div>
|
||||
</div>
|
||||
{!compact && (
|
||||
<span className="name" title={curr?.name}>
|
||||
<span className={nameClass} title={curr?.name}>
|
||||
{curr?.name}
|
||||
</span>
|
||||
)}
|
||||
{owner && <IconOwner />}
|
||||
{curr.is_bot && <IconBot className={clsx(compact && "absolute -top-1 -right-1", "!w-4 !h-4")} />}
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
</ContextMenu>
|
||||
);
|
||||
return (
|
||||
@@ -94,30 +98,30 @@ const User: FC<Props> = ({
|
||||
trigger="click"
|
||||
content={<Profile uid={uid} type="card" cid={cid} />}
|
||||
>
|
||||
<StyledWrapper
|
||||
size={avatarSize}
|
||||
className={`${interactive ? "interactive" : ""} ${compact ? "compact" : ""}`}
|
||||
<div
|
||||
className={containerClass}
|
||||
onDoubleClick={dm ? handleDoubleClick : undefined}
|
||||
onContextMenu={enableContextMenu ? handleContextMenuEvent : undefined}
|
||||
>
|
||||
<div className="avatar">
|
||||
<div className="cursor-pointer relative" style={{ width: `${avatarSize}px`, height: `${avatarSize}px` }}>
|
||||
<Avatar
|
||||
className="w-full h-full rounded-full object-cover"
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
src={curr.avatar}
|
||||
name={curr.name}
|
||||
alt="avatar"
|
||||
/>
|
||||
<div className={`status ${online ? "online" : "offline"}`}></div>
|
||||
<div className={statusClass}></div>
|
||||
</div>
|
||||
{!compact && (
|
||||
<span className="name" title={curr?.name}>
|
||||
<span className={nameClass} title={curr?.name}>
|
||||
{curr?.name}
|
||||
</span>
|
||||
)}
|
||||
{owner && <IconOwner />}
|
||||
{curr.is_bot && <IconBot className="!w-4 !h-4" />}
|
||||
</StyledWrapper>
|
||||
</div>
|
||||
</Tippy>
|
||||
</ContextMenu>
|
||||
);
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
&.interactive {
|
||||
&:hover,
|
||||
&.active {
|
||||
background: rgba(116, 127, 141, 0.1);
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
width: ${({ size }: { size: number }) => `${size}px`};
|
||||
height: ${({ size }: { size: number }) => `${size}px`};
|
||||
position: relative;
|
||||
img {
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.status {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -6px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
box-sizing: content-box;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
&.online {
|
||||
background-color: #22c55e;
|
||||
}
|
||||
&.offline {
|
||||
background-color: #a1a1aa;
|
||||
}
|
||||
&.alert {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
}
|
||||
}
|
||||
.name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #52525b;
|
||||
max-width: 190px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
/* session nav */
|
||||
&.compact {
|
||||
padding: 0;
|
||||
.avatar .status {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -160,7 +160,7 @@ export function sliceFile(file: File | null, chunksAmount: number) {
|
||||
|
||||
return chunks;
|
||||
}
|
||||
export const getFileIcon = (type: string, name = "") => {
|
||||
export const getFileIcon = (type: string, name = "", className = "icon") => {
|
||||
let icon = null;
|
||||
|
||||
const checks = {
|
||||
@@ -176,27 +176,27 @@ export const getFileIcon = (type: string, name = "") => {
|
||||
switch (true) {
|
||||
case checks.image.test(_type):
|
||||
{
|
||||
icon = <IconImage className="icon" />;
|
||||
icon = <IconImage className={className} />;
|
||||
}
|
||||
break;
|
||||
case checks.pdf.test(_type):
|
||||
icon = <IconPdf className="icon" />;
|
||||
icon = <IconPdf className={className} />;
|
||||
break;
|
||||
case checks.code.test(_type):
|
||||
icon = <IconCode className="icon" />;
|
||||
icon = <IconCode className={className} />;
|
||||
break;
|
||||
case checks.doc.test(_type):
|
||||
icon = <IconDoc className="icon" />;
|
||||
icon = <IconDoc className={className} />;
|
||||
break;
|
||||
case checks.audio.test(_type):
|
||||
icon = <IconAudio className="icon" />;
|
||||
icon = <IconAudio className={className} />;
|
||||
break;
|
||||
case checks.video.test(_type):
|
||||
icon = <IconVideo className="icon" />;
|
||||
icon = <IconVideo className={className} />;
|
||||
break;
|
||||
|
||||
default:
|
||||
icon = <IconUnknown className="icon" />;
|
||||
icon = <IconUnknown className={className} />;
|
||||
break;
|
||||
}
|
||||
return icon;
|
||||
|
||||
Reference in New Issue
Block a user