refactor: more tailwind

This commit is contained in:
Tristan Yang
2023-02-07 09:25:05 +08:00
parent 07ddfbf12b
commit c5669f8bc4
48 changed files with 447 additions and 1314 deletions
+28 -20
View File
@@ -1,4 +1,4 @@
import { FC, ImgHTMLAttributes } from "react";
import { FC, ImgHTMLAttributes, useEffect, useState } from "react";
import { getInitials } from "../utils";
interface Props extends ImgHTMLAttributes<HTMLImageElement> {
@@ -29,26 +29,34 @@ const Avatar: FC<Props> = ({
height,
...rest
}) => {
if (src && src.length !== 0) {
return <img src={src} {...rest} />;
} else {
return (
<div
className={`rounded-full flex-center ${rest.className || ""}`}
style={{
width,
height,
fontSize: getFontSize(width),
fontWeight: 400,
fontFamily: "'Lato', 'Lato-Regular', 'Helvetica Neue'",
background: type === "channel" ? "#EAECF0" : "#4c99e9",
color: type === "channel" ? "#475467" : "#FFFFFF"
}}
>
{getInitials(name)}
</div>
);
const [avatarSrc, setAvatarSrc] = useState("");
useEffect(() => {
if (!src) {
setAvatarSrc(src);
}
}, [src]);
const handleError = () => {
setAvatarSrc("");
};
if (avatarSrc) {
return <img src={avatarSrc} onError={handleError} {...rest} />;
}
return (
<div
className={`rounded-full flex-center ${rest.className || ""}`}
style={{
width,
height,
fontSize: getFontSize(width),
fontWeight: 400,
fontFamily: "'Lato', 'Lato-Regular', 'Helvetica Neue'",
background: type === "channel" ? "#EAECF0" : "#4c99e9",
color: type === "channel" ? "#475467" : "#FFFFFF"
}}
>
{getInitials(name)}
</div>
);
};
export default Avatar;
+4 -4
View File
@@ -139,18 +139,18 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
? t("create_private_channel_desc")
: t("create_channel_desc")}
</p>
<div className="w-full flex flex-col justify-start gap-2 mb-8.5">
<div className="w-full flex flex-col justify-start gap-2 mb-8">
<span className="text-gray-400 text-sm font-normal">{t("channel_name")}</span>
<div className="relative">
<input className="text-gray-600 rounded p-2 pl-9 border border-solid border-gray-300 w-full" onChange={handleNameInput} value={name} placeholder="new channel" />
<ChannelIcon personal={!is_public} className="absolute left-2 top-1/2 -translate-y-1/2" />
</div>
</div>
<div className="w-full flex items-center justify-between mb-12.5">
<div className="w-full flex items-center justify-between mb-12">
<span className="text-gray-400 text-sm">{t("private_channel")}</span>
<StyledToggle
data-checked={!is_public}
data-disabled={!loginUser?.is_admin}
checked={!is_public}
disabled={!loginUser?.is_admin}
onClick={handleToggle}
/>
</div>
+2 -3
View File
@@ -1,5 +1,4 @@
import { FC, ReactElement } from "react";
import StyledMenu from "./styled/Menu";
export interface Item {
title: string;
@@ -16,7 +15,7 @@ interface Props {
const ContextMenu: FC<Props> = ({ items = [], hideMenu = null }) => {
return (
<StyledMenu>
<ul className="context-menu">
{items.map((item) => {
// if (!item) return null;
const {
@@ -49,7 +48,7 @@ const ContextMenu: FC<Props> = ({ items = [], hideMenu = null }) => {
</li>
);
})}
</StyledMenu>
</ul>
);
};
+18 -18
View File
@@ -1,6 +1,6 @@
import { FC, ReactElement } from "react";
import dayjs from "dayjs";
import Styled from "./styled";
import clsx from "clsx";
import {
VideoPreview,
AudioPreview,
@@ -80,33 +80,33 @@ const FileBox: FC<Props> = ({
content
}) => {
const fromUser = useAppSelector((store) => store.users.byId[from_uid]);
const icon = getFileIcon(file_type, name);
const icon = getFileIcon(file_type, name, "icon w-9 h-12");
if (!content || !fromUser || !name) return null;
const previewContent = renderPreview({ file_type, content, name });
const withPreview = preview && previewContent;
return (
<Styled
className={`file_box ${flex ? "flex" : ""} ${withPreview ? "preview" : ""} ${file_type.startsWith("audio") ? "audio" : ""
}`}
<div
className={clsx(`h-[66px] rounded-md border border-solid border-gray-300 bg-gray-100`, flex ? "w-full" : "w-[370px]", withPreview && "relative overflow-hidden h-[281px]", file_type.startsWith("audio") && "h-[125px]")}
>
<div className="basic">
<div className="w-full p-2 flex items-center justify-between gap-2">
{icon}
<div className="info">
<span className="name">{name}</span>
<span className="details">
<i className="size">{formatBytes(size)}</i>
<i className="time">{dayjs(created_at).fromNow()}</i>
<i className="from">
by <strong>{fromUser.name}</strong>
</i>
</span>
<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>
<em className="text-xs text-gray-500 flex gap-4 not-italic">
<span className="size">{formatBytes(size)}</span>
<span className="time">{dayjs(created_at).fromNow()}</span>
<span>
by <strong className="font-bold">{fromUser.name}</strong>
</span>
</em>
</div>
<a className="download" download={name} href={`${content}&download=true`}>
<a className="whitespace-nowrap" download={name} href={`${content}&download=true`}>
<IconDownload className="fill-gray-500" />
</a>
</div>
{withPreview && <div className="preview">{previewContent}</div>}
</Styled>
{withPreview && <div className="h-[calc(100%_-_64px)] overflow-hidden">{previewContent}</div>}
</div>
);
};
-72
View File
@@ -1,72 +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;
* {
user-select: text;
}
&.flex {
width: 100%;
}
&.preview {
position: relative;
overflow: hidden;
height: 281px;
&.audio {
height: 125px;
}
}
.basic {
width: 100%;
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 {
font-weight: 400;
font-size: 12px;
line-height: 18px;
color: #616161;
display: flex;
gap: 16px;
.from strong {
font-weight: bold;
}
}
}
.download {
white-space: nowrap;
}
}
.preview {
height: calc(100% - 64px);
overflow: hidden;
/* todo */
}
`;
export default Styled;
+21 -19
View File
@@ -1,25 +1,26 @@
import { useState, MouseEvent, ChangeEvent, FC } from "react";
// import toast from "react-hot-toast";
import toast from "react-hot-toast";
import Modal from "../Modal";
import Button from "../styled/Button";
import Input from "../styled/Input";
import Channel from "../Channel";
import User from "../User";
import Reply from "../Message/Reply";
import StyledWrapper from "./styled";
import useForwardMessage from "../../hook/useForwardMessage";
import useSendMessage from "../../hook/useSendMessage";
import useFilteredChannels from "../../hook/useFilteredChannels";
import useFilteredUsers from "../../hook/useFilteredUsers";
import CloseIcon from "../../../assets/icons/close.circle.svg";
import StyledCheckbox from "../styled/Checkbox";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
interface IProps {
mids: number[];
closeModal: () => void;
}
const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
const { t } = useTranslation();
const [appendText, setAppendText] = useState("");
const { sendMessages } = useSendMessage();
const { forwardMessage, forwarding } = useForwardMessage();
@@ -74,16 +75,17 @@ const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
(selectedChannels.length == 0 && selectedMembers.length == 0) || forwarding;
return (
<Modal>
<StyledWrapper>
<div className="left">
<div className="search">
<div className="flex max-h-[514px] min-h-[400px] bg-white drop-shadow rounded-lg overflow-hidden">
<div className="w-[271px] shadow-[inset_-1px_0px_0px_rgba(0,_0,_0,_0.1)] overflow-y-scroll bg-inherit">
<div className="sticky top-0 bg-inherit z-[90] p-4 w-[calc(100%_-_1px)]">
<input
className="px-2 py-2.5 text-sm bg-black/10 rounded-lg w-full"
value={input}
onChange={handleSearchChange}
placeholder="Search user or channel"
/>
</div>
<ul className="users">
<ul className="flex flex-col pb-5">
{channels &&
channels.map((c) => {
const { gid } = c;
@@ -93,7 +95,7 @@ const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
key={gid}
data-type="channel"
data-id={gid}
className="user channel"
className="cursor-pointer flex items-center px-4 rounded hover:bg-gray-600/10"
onClick={toggleCheck}
>
<StyledCheckbox readOnly checked={checked} name="cb" id="cb" />
@@ -110,7 +112,7 @@ const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
key={uid}
data-id={uid}
data-type="user"
className="user"
className="cursor-pointer flex items-center px-4 rounded hover:bg-gray-600/10"
onClick={toggleCheck}
>
<StyledCheckbox readOnly checked={checked} name="cb" id="cb" />
@@ -120,15 +122,15 @@ const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
})}
</ul>
</div>
<div className={`right`}>
<h3 className="title">Send To {selectedCount}</h3>
<ul className="selected">
<div className={`flex flex-col items-start p-4 box-border`}>
<h3 className="font-semibold text-sm text-gray-700 mb-4">Send To {selectedCount}</h3>
<ul className="w-full h-[260px] py-2.5 overflow-y-scroll">
{selectedChannels.map((cid) => {
return (
<li key={cid} className="item">
<li key={cid} className="relative">
<Channel key={cid} id={cid} interactive={false} />
<CloseIcon
className="remove"
className="cursor-pointer absolute right-1 top-1/2 -translate-y-1/2"
onClick={removeSelected.bind(null, cid, "channel")}
/>
</li>
@@ -143,27 +145,27 @@ const ForwardModal: FC<IProps> = ({ mids, closeModal }) => {
);
})}
</ul>
<div className="msgs">
<div className="rounded-lg p-2 max-h-[200px] overflow-auto bg-slate-100 w-[280px] mb-1">
{mids.map((mid) => (
<Reply key={mid} mid={mid} interactive={false} />
))}
</div>
<Input
className="input"
className="mb-8"
placeholder="Leave a message"
value={appendText}
onChange={updateAppendText}
></Input>
<div className="btns">
<div className="w-full flex items-center justify-end gap-4">
<Button onClick={closeModal} className="normal cancel">
Cancel
{t('action.cancel')}
</Button>
<Button className="normal" disabled={sendButtonDisabled} onClick={handleForward}>
Send To {selectedCount == 0 ? null : `(${selectedCount})`}
</Button>
</div>
</div>
</StyledWrapper>
</div>
</Modal>
);
};
@@ -1,107 +0,0 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
display: flex;
max-height: 514px;
min-height: 400px;
background: #fff;
box-shadow: 0px 25px 50px rgba(31, 41, 55, 0.25);
border-radius: var(--br);
transition: all 0.5s ease;
overflow: hidden;
.left {
width: 276px;
box-shadow: inset -1px 0px 0px rgba(0, 0, 0, 0.1);
overflow-y: scroll;
.search {
position: sticky;
top: 0;
z-index: 99;
background: #fff;
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1);
padding: 16px;
width: calc(100% - 1px);
input {
outline: none;
width: -webkit-fill-available;
padding: 10px 8px;
font-size: 14px;
line-height: 20px;
background: rgba(0, 0, 0, 0.08);
border-radius: var(--br);
}
}
.users {
display: flex;
flex-direction: column;
padding-bottom: 20px;
.user {
cursor: pointer;
display: flex;
align-items: center;
padding: 0 16px;
width: -webkit-fill-available;
border-radius: 4px;
&:hover {
background: rgba(116, 127, 141, 0.1);
}
> div {
width: 100%;
}
}
}
}
.right {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 16px;
box-sizing: border-box;
.title {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #344054;
margin-bottom: 16px;
}
.selected {
width: 100%;
height: 260px;
padding: 10px 0;
overflow: scroll;
.item {
position: relative;
.remove {
cursor: pointer;
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
}
}
}
.msgs {
border-radius: var(--br);
padding: 8px;
max-height: 200px;
overflow: auto;
background-color: #f4f4f5;
width: 280px;
margin-bottom: 4px;
> .reply {
background: none;
}
}
.input {
margin-bottom: 32px;
}
.btns {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
}
}
`;
export default StyledWrapper;
+1 -1
View File
@@ -45,7 +45,7 @@ const GithubLoginButton: FC<Props> = ({ type = "login", source = "webapp", clien
};
return (
<Button className="flex-center gap-3 ghost !text-gray-600 !border-[#d0d5dd]" onClick={handleGithubLogin}>
<Button className="flex-center gap-3 ghost !text-gray-600 dark:!text-gray-100 !border-[#d0d5dd]" onClick={handleGithubLogin}>
<IconGithub className="w-6 h-6" />
{` ${type === "login" ? t("login.github") : t("reg.github")}`}
</Button>
+4 -5
View File
@@ -4,7 +4,6 @@ import { hideAll } from "tippy.js";
import toast from "react-hot-toast";
import { useUpdateUserMutation } from "../../../app/services/user";
import User from "../User";
import StyledMenu from "../styled/Menu";
import InviteLink from "../InviteLink";
import moreIcon from "../../../assets/icons/more.svg?url";
import IconOwner from "../../../assets/icons/owner.svg";
@@ -93,7 +92,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
placement="bottom-end"
trigger="click"
content={
<StyledMenu className="menu">
<ul className="context-menu">
<li
className="item sb"
onClick={handleToggleRole.bind(null, {
@@ -116,7 +115,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
{t("user")}
{!is_admin && <IconCheck className="icon" />}
</li>
</StyledMenu>
</ul>
}
>
<IconArrowDown className="cursor-pointer dark:fill-slate-50" />
@@ -129,7 +128,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
placement="right-start"
trigger="click"
content={
<StyledMenu className="min-w-30">
<ul className="min-w-30 context-menu">
{email && (
<li className="item" onClick={copyEmail.bind(null, email)}>
{ct("action.copy_email")}
@@ -145,7 +144,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
{ct("action.remove")}
</li>
)}
</StyledMenu>
</ul>
}
>
<div className="relative w-6 h-6">
+10 -50
View File
@@ -1,51 +1,10 @@
import { useState, useRef, useEffect, ChangeEvent, KeyboardEvent, FC } from "react";
import styled from "styled-components";
import TextareaAutosize from "react-textarea-autosize";
import { useKey } from "rooks";
import { useEditMessageMutation } from "../../../app/services/message";
import { ContentTypes } from "../../../app/config";
import { useAppSelector } from "../../../app/store";
const StyledWrapper = styled.div`
width: 100%;
.input {
background: #e5e7eb;
border-radius: 8px;
padding: 16px;
textarea {
outline: none;
width: 100%;
background: none;
resize: unset;
user-select: text;
color: #374151;
font-weight: normal;
font-size: 14px;
line-height: 20px;
word-break: break-all;
white-space: break-spaces;
}
}
.opts {
padding: 4px;
display: flex;
align-items: center;
gap: 16px;
.opt {
font-weight: normal;
font-size: 12px;
line-height: 18px;
button {
padding: 0 4px;
font-size: inherit;
line-height: inherit;
background: none;
cursor: pointer;
color: #06b6d4;
}
}
}
`;
type Props = {
mid: number;
cancelEdit: () => void;
@@ -97,9 +56,10 @@ const EditMessage: FC<Props> = ({ mid, cancelEdit }) => {
};
if (!msg) return null;
return (
<StyledWrapper>
<div className="input">
<div className="w-full">
<div className="bg-gray-200 rounded-lg p-4">
<TextareaAutosize
autoFocus
onFocus={(e) =>
@@ -109,7 +69,7 @@ const EditMessage: FC<Props> = ({ mid, cancelEdit }) => {
)
}
ref={inputRef}
className="content"
className="content w-full resize-none bg-transparent text-gray-800 text-sm break-all"
maxRows={8}
minRows={1}
onKeyDown={handleInputKeydown}
@@ -118,15 +78,15 @@ const EditMessage: FC<Props> = ({ mid, cancelEdit }) => {
placeholder={`Edit Message`}
/>
</div>
<div className="opts">
<span className="opt">
esc to <button onClick={cancelEdit}>cancel</button>
<div className="flex items-center p-1 gap-4 text-xs">
<span>
esc to <button className="text-primary-500 cursor-pointer px-1" onClick={cancelEdit}>cancel</button>
</span>
<span className="opt">
enter to <button onClick={handleSave}>{isEditing ? "saving" : `save`}</button>
<span>
enter to <button className="text-primary-500 cursor-pointer px-1" onClick={handleSave}>{isEditing ? "saving" : `save`}</button>
</span>
</div>
</StyledWrapper>
</div>
);
};
export default EditMessage;
@@ -1,5 +1,4 @@
import { useEffect, useState, FC, ReactElement } from "react";
import styled from "styled-components";
import StyledMsg from "./styled";
import renderContent from "./renderContent";
import Avatar from "../Avatar";
@@ -7,29 +6,6 @@ import IconForward from "../../../assets/icons/forward.svg";
import useNormalizeMessage from "../../hook/useNormalizeMessage";
import { useTranslation } from "react-i18next";
const StyledForward = styled.div`
display: flex;
flex-direction: column;
border-radius: var(--br);
background-color: #f4f4f5;
> .tip {
padding: 8px 8px 0 8px;
display: flex;
align-items: center;
gap: 4px;
.icon {
width: 16px;
height: 16px;
path {
fill: #98a2b3;
}
}
font-weight: 400;
font-size: 12px;
line-height: 18px;
color: #98a2b3;
}
`;
type Props = {
context: "user" | "channel";
to: number;
@@ -45,15 +21,14 @@ const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
normalizeMessage(id);
}
}, [id]);
useEffect(() => {
if (messages) {
const forward_mids = messages.map(({ from_mid }) => from_mid) || [];
// console.log("fff", messages);
setForwards(
<StyledForward data-forwarded-mids={forward_mids.join(",")}>
<h4 className="tip">
<IconForward className="icon" />
<div data-forwarded-mids={forward_mids.join(",")} className="flex flex-col rounded-lg bg-gray-100">
<h4 className="p-2 pb-0 flex items-center gap-1 text-gray-500 text-xs">
<IconForward className="w-4 h-4 fill-gray-500" />
{t("action.forward")}
</h4>
<div className="list">
@@ -87,7 +62,7 @@ const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
);
})}
</div>
</StyledForward>
</div>
);
}
}, [messages, context, to, from_uid]);
+13 -106
View File
@@ -1,108 +1,13 @@
import { FC } from "react";
import styled from "styled-components";
import Tippy from "@tippyjs/react";
import { hideAll } from "tippy.js";
import ReactionItem, { Emojis, ReactionMap } from "../ReactionItem";
import ReactionPicker from "./ReactionPicker";
import Tooltip from "../Tooltip";
import { useReactMessageMutation } from "../../../app/services/message";
import addEmojiIcon from "../../../assets/icons/add.emoji.svg?url";
import IconAddEmoji from "../../../assets/icons/add.emoji.svg";
import { useAppSelector } from "../../../app/store";
const StyledWrapper = styled.span`
position: relative;
margin-top: 8px;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 4px;
width: fit-content;
.reaction {
cursor: pointer;
background-color: #ecfdff;
border-radius: 6px;
position: relative;
display: flex;
align-items: center;
gap: 4px;
padding: 4px;
> .emoji {
> * {
display: flex;
}
}
&:hover {
background-color: #cff9fe;
}
&.reacted {
box-shadow: inset 0 0 0 1px #06aed4;
background-color: #a5f0fc;
}
> .count {
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: #06aed4;
}
}
> .add {
visibility: hidden;
width: 24px;
height: 24px;
background-color: #ecfdff;
border-radius: 6px;
border: none;
background-image: url(${addEmojiIcon});
background-size: 16px;
background-repeat: no-repeat;
background-position: center;
&:hover {
background-color: #cff9fe;
}
}
&:hover > .add {
visibility: visible;
}
`;
const StyledDetails = styled.div`
position: relative;
background: #ffffff;
border-radius: var(--br);
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px;
&:after {
content: "";
display: block;
width: 12px;
height: 12px;
background-color: #fff;
border-radius: 1px;
position: absolute;
bottom: -6px;
left: calc(50% - 6px);
transform: matrix(0.71, 0.71, -0.71, 0.71, 0, 0);
}
&.first:after {
left: calc(50% - 16px);
}
.emoji {
width: 32px;
height: 32px;
}
.desc {
display: flex;
flex-direction: column;
width: 140px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
color: #1d2939;
}
`;
const ReactionDetails = ({
uids = [],
@@ -123,15 +28,15 @@ const ReactionDetails = ({
? `${names.join(", ")} and ${names.length - 3} others reacted with`
: `${names.join(", ")} reacted with`;
return (
<StyledDetails className={index == 0 ? "first" : ""}>
<div className="emoji">
<div className={`relative bg-white rounded-lg shadow flex items-start gap-2 p-2 ${index == 0 ? "first" : ""}`}>
<div className="w-8 h-8">
<ReactionItem native={emoji} />
</div>
<div className="desc">
<div className="flex flex-col w-[140px] text-xs text-gray-800">
<span>{prefixDesc}</span>
<span>{ReactionMap[emoji]}</span>
</div>
</StyledDetails>
</div>
);
};
type Props = {
@@ -152,15 +57,15 @@ const Reaction: FC<Props> = ({ mid, reactions = null, readOnly = false }) => {
reactWithEmoji({ mid, action: emoji });
};
if (!reactions || Object.entries(reactions).length == 0) return null;
return (
<StyledWrapper className="reactions">
<span className="group relative mt-2 mb-1 flex items-center gap-1 w-fit">
{Object.entries(reactions).map(([reaction, uids], idx) => {
const reacted = uids.findIndex((id: number) => id == currUid) > -1;
return uids.length > 0 ? (
<span
onClick={readOnly ? undefined : handleReact.bind(null, reaction)}
className={`reaction ${reacted ? "reacted" : ""}`}
// data-count={count > 1 ? count : ""}
className={`cursor-pointer rounded-md relative flex items-center gap-1 p-1 hover:bg-[#cff9fe] ${reacted ? "shadow-[inset_0_0_0_1px_#06aed4] bg-[#a5f0fc]" : ""}`}
key={reaction}
>
<Tippy
@@ -176,7 +81,7 @@ const Reaction: FC<Props> = ({ mid, reactions = null, readOnly = false }) => {
</i>
</Tippy>
{uids.length > 1 ? <em className="count">{`${uids.length}`} </em> : null}
{uids.length > 1 ? <em className="text-primary-600 text-xs">{`${uids.length}`} </em> : null}
</span>
) : null;
})}
@@ -188,11 +93,13 @@ const Reaction: FC<Props> = ({ mid, reactions = null, readOnly = false }) => {
trigger="click"
content={<ReactionPicker mid={mid} hidePicker={hideAll} />}
>
<button className="add"></button>
<button className="invisible group-hover:visible w-6 h-6 bg-[#ecfdff] hover:bg-[#cff9fe] rounded-md flex-center">
<IconAddEmoji className={'w-4 h-4'} />
</button>
</Tippy>
</Tooltip>
)}
</StyledWrapper>
</span>
);
};
export default Reaction;
@@ -1,40 +1,9 @@
import { FC } from "react";
import styled from "styled-components";
import { useReactMessageMutation } from "../../../app/services/message";
import { Emojis } from "../../../app/config";
import Emoji from "../ReactionItem";
import { useAppSelector } from "../../../app/store";
const StyledPicker = styled.div`
background: none;
z-index: 999;
.emojis {
padding: 4px;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
background: #fff;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 12px;
&.reacting {
opacity: 0.6;
}
.wrapper {
display: flex;
cursor: pointer;
border-radius: 8px;
padding: 4px;
&:hover,
&.reacted {
background-color: #f5f6f7;
}
> .emoji {
width: 16px;
height: 16px;
}
}
}
`;
type Props = {
mid: number;
hidePicker: () => void;
@@ -52,15 +21,14 @@ const ReactionPicker: FC<Props> = ({ mid, hidePicker }) => {
hidePicker();
};
return (
<StyledPicker>
<ul className={`emojis ${isLoading ? "reacting" : ""}`}>
<div className="z-[999]">
<ul className={`p-1 grid grid-cols-[repeat(4,_1fr)] gap-2 bg-white dark:bg-gray-900 drop-shadow-md rounded-xl ${isLoading ? "opacity-60" : ""}`}>
{Emojis.map((emoji) => {
let reacted =
reactionData[emoji] && reactionData[emoji].findIndex((id) => id == currUid) > -1;
return (
<li
className={`wrapper ${reacted ? "reacted" : ""}`}
className={`flex-center cursor-pointer rounded-lg p-4 hover:bg-gray-50 w-4 h-4 ${reacted ? "bg-gray-50" : ""}`}
key={emoji}
onClick={handleReact.bind(null, emoji)}
>
@@ -69,7 +37,7 @@ const ReactionPicker: FC<Props> = ({ mid, hidePicker }) => {
);
})}
</ul>
</StyledPicker>
</div>
);
};
export default ReactionPicker;
+13 -101
View File
@@ -1,5 +1,4 @@
import React, { MouseEvent, FC } from "react";
import styled from "styled-components";
import MarkdownRender from "../MarkdownRender";
import { ContentTypes } from "../../../app/config";
import { getFileIcon, isImage } from "../../utils";
@@ -8,94 +7,6 @@ import LinkifyText from '../LinkifyText';
import Avatar from "../Avatar";
import { useAppSelector } from "../../../app/store";
import { MessagePayload } from "../../../app/slices/message";
const Styled = styled.div`
display: flex;
align-items: flex-start;
padding: 8px;
background: #e5e7eb;
border-radius: var(--br);
gap: 8px;
margin-bottom: 4px;
&.clickable {
cursor: pointer;
}
.user {
display: flex;
align-items: center;
gap: 4px;
white-space: nowrap;
.avatar {
width: 16px;
height: 16px;
border-radius: 50%;
}
.name {
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #06b6d4;
}
}
.content {
overflow: hidden;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #616161;
display: flex;
align-items: center;
.txt {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
text-overflow: ellipsis;
overflow: hidden;
word-wrap: break-word;
word-break: break-all;
}
.md {
position: relative;
max-height: 152px;
overflow: hidden;
&:after {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
content: "";
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 63.54%, #e5e7eb 93.09%);
}
}
.pic {
display: inherit;
width: 40px;
height: 40px;
object-fit: cover;
}
.icon {
width: 15px;
height: 20px;
}
.file_name {
margin-left: 5px;
font-size: 10px;
color: #555;
}
}
`;
const renderContent = (data: MessagePayload) => {
const { content_type, content, thumbnail, properties } = data;
@@ -103,14 +14,14 @@ const renderContent = (data: MessagePayload) => {
switch (content_type) {
case ContentTypes.text:
res = (
<span className="txt">
<span className="text-ellipsis overflow-hidden break-words break-all text-gray-800">
<LinkifyText text={content} url={false} mentionTextOnly={true} mentionPopOver={false} />
</span>
);
break;
case ContentTypes.markdown:
res = (
<div className="md">
<div className="max-h-[152px] overflow-hidden">
<MarkdownRender content={content} />
</div>
);
@@ -118,14 +29,14 @@ const renderContent = (data: MessagePayload) => {
case ContentTypes.file:
{
const { content_type = "", name, size } = properties || {};
const icon = getFileIcon(content_type, name);
const icon = getFileIcon(content_type, name, "w-4 h-5");
if (isImage(content_type, size)) {
res = <img className="pic" src={thumbnail} />;
res = <img className="w-10 h-10 object-cover" src={thumbnail} />;
} else {
res = (
<>
{icon}
<span className="file_name">{name}</span>
<span className="ml-1 text-[10px] text-gray-500">{name}</span>
</>
);
}
@@ -161,25 +72,26 @@ const Reply: FC<ReplyProps> = ({ mid, interactive = true }) => {
if (!data) return null;
const currUser = users[data.from_uid || 0];
if (!currUser) return null;
return (
<Styled
<div
key={mid}
data-mid={mid}
className={`reply ${interactive ? "clickable" : ""}`}
className={`flex items-start p-2 bg-gray-100 rounded-lg gap-2 mb-1 ${interactive ? "cursor-pointer" : "!bg-transparent"}`}
onClick={interactive ? handleClick : undefined}
>
<div className="user">
<div className="flex items-center gap-1 whitespace-nowrap">
<Avatar
width={16}
height={16}
className="avatar"
className="w-4 h-4 rounded-full"
src={currUser.avatar}
name={currUser.name}
/>
<span className="name">{currUser.name}</span>
<span className="text-sm text-primary-500">{currUser.name}</span>
</div>
<div className="content">{renderContent(data)}</div>
</Styled>
<div className="text-sm flex items-center text-gray-400 overflow-hidden">{renderContent(data)}</div>
</div>
);
};
+1 -1
View File
@@ -188,7 +188,7 @@ const Plugins: FC<Props> = ({
return (
<div className="input max-h-[50vh] overflow-auto text-sm text-gray-600 dark:text-white" ref={editableRef}>
<div className="input w-full max-h-[50vh] overflow-auto text-sm text-gray-600 dark:text-white" ref={editableRef}>
<Plate
id={`${TEXT_EDITOR_PREFIX}_${id}`}
onChange={handleChange}
+2 -3
View File
@@ -4,7 +4,6 @@ import Tippy from "@tippyjs/react";
import IconMessage from "../../../assets/icons/message.svg";
import IconMore from "../../../assets/icons/more.svg";
import Avatar from "../Avatar";
import StyledMenu from "../styled/Menu";
import useUserOperation from "../../hook/useUserOperation";
import { useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next";
@@ -76,7 +75,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
trigger="click"
hideOnClick={true}
content={
<StyledMenu>
<ul className="context-menu">
{canCopyEmail && (
<li className="item" onClick={copyEmail.bind(undefined, email)}>
{t("copy_email")}
@@ -92,7 +91,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
{t("remove")}
</li>
)}
</StyledMenu>
</ul>
}
>
<li className={`${iconClass} icon ${hasMore ? "" : "text-gray-500"}`}>
+8 -8
View File
@@ -29,14 +29,14 @@ export const ReactionMap = {
"🚀": ":rocket:",
};
const emojis: Emojis = {
"👍": <EmojiThumbUp className="emoji" />,
"👎": <EmojiThumbDown className="emoji" />,
"😄": <EmojiSmile className="emoji" />,
"👀": <EmojiLook className="emoji" />,
"🚀": <EmojiRocket className="emoji" />,
"❤️": <EmojiHeart className="emoji" />,
"🙁": <EmojiUnhappy className="emoji" />,
"🎉": <EmojiCelebrate className="emoji" />
"👍": <EmojiThumbUp className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
"👎": <EmojiThumbDown className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
"😄": <EmojiSmile className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
"👀": <EmojiLook className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
"🚀": <EmojiRocket className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
"❤️": <EmojiHeart className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
"🙁": <EmojiUnhappy className="emoji w-full h-full min-w-[16px] min-h-[16px]" />,
"🎉": <EmojiCelebrate className="emoji w-full h-full min-w-[16px] min-h-[16px]" />
};
interface Props {
+12 -79
View File
@@ -1,4 +1,3 @@
import styled from "styled-components";
import { ContentTypes } from "../../../app/config";
import MarkdownRender from "../MarkdownRender";
import closeIcon from "../../../assets/icons/close.circle.svg?url";
@@ -9,73 +8,6 @@ import { useAppSelector } from "../../../app/store";
import { MessagePayload } from "../../../app/slices/message";
import LinkifyText from "../LinkifyText";
const Styled = styled.div`
background-color: #f3f4f6;
z-index: 999;
display: flex;
align-items: flex-start;
justify-content: flex-start;
gap: 16px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
width: 100%;
padding: 12px 16px;
.prefix {
white-space: nowrap;
color: #667085;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
em {
font-weight: bold;
color: #363f53;
}
}
.content {
white-space: normal;
color: #616161;
overflow: hidden;
padding-right: 30px;
font-weight: 500;
font-size: 14px;
line-height: 20px;
> .pic {
width: 40px;
height: 40px;
object-fit: cover;
}
.md {
position: relative;
max-height: 100px;
overflow: hidden;
&:after {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
content: "";
background: linear-gradient(180deg, rgba(255, 255, 255, 0) 63.54%, #f3f4f6 93.09%);
}
}
.icon {
width: 15px;
height: 20px;
}
.name {
margin-left: 5px;
font-size: 10px;
color: #555;
}
}
.close {
background: none;
position: absolute;
top: 16px;
right: 16px;
}
`;
const renderContent = (data: MessagePayload) => {
const { content_type, content, thumbnail = "", properties } = data;
let res = null;
@@ -87,9 +19,10 @@ const renderContent = (data: MessagePayload) => {
// return <Mention popover={false} key={idx} uid={+uid} />;
// });
break;
case ContentTypes.markdown:
res = (
<div className="md">
<div className="max-h-[100px] overflow-auto">
<MarkdownRender content={content} />
</div>
);
@@ -100,13 +33,13 @@ const renderContent = (data: MessagePayload) => {
const image = isImage(content_type, size);
// console.log("replying data", content_type, size, image);
if (image) {
res = <img className="pic" src={thumbnail || pictureIcon} />;
res = <img className="w-10 h-10 object-cover" src={thumbnail || pictureIcon} />;
} else {
const icon = getFileIcon(content_type, name);
const icon = getFileIcon(content_type, name, "icon w-4 h-5");
res = (
<>
{icon}
<span className="name">{name}</span>
<span className="ml-1 text-[10px] text-gray-400">{name}</span>
</>
);
}
@@ -118,7 +51,6 @@ const renderContent = (data: MessagePayload) => {
// console.log("replying data", data);
return res;
};
export default function Replying({
context,
id,
@@ -138,15 +70,16 @@ export default function Replying({
if (!msg) return null;
const { from_uid = 0 } = msg;
const user = usersData[from_uid];
return (
<Styled className="reply">
<div className="prefix">
Replying to <em>{user?.name}</em>
<div className="reply bg-gray-100 z-[999] flex items-start justify-start gap-4 rounded-t-lg w-full px-4 py-3 text-sm">
<div className="whitespace-nowrap text-gray-400 ">
Replying to <span className="font-bold text-gray-600">{user?.name}</span>
</div>
<div className="content">{renderContent(msg)}</div>
<button className="close" onClick={removeReply}>
<div className="text-gray-500 overflow-hidden pr-7 ">{renderContent(msg)}</div>
<button className="absolute top-4 right-4 cursor-pointer" onClick={removeReply}>
<img src={closeIcon} alt="close icon" />
</button>
</Styled>
</div>
);
}
+9 -7
View File
@@ -1,10 +1,12 @@
import { useEffect, useState, FC } from "react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import useSendMessage from "../../hook/useSendMessage";
import useAddLocalFileMessage from "../../hook/useAddLocalFileMessage";
import { updateInputMode } from "../../../app/slices/ui";
import { ContentTypes, ChatPrefixes } from "../../../app/config";
import StyledSend from "./styled";
// import StyledSend from "./styled";
import UploadFileList from "./UploadFileList";
import Replying from "./Replying";
import Toolbar from "./Toolbar";
@@ -15,7 +17,6 @@ import MixedInput, { useMixedEditor } from "../MixedInput";
import useDraft from "../../hook/useDraft";
import useUploadFile from "../../hook/useUploadFile";
import { useAppDispatch, useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next";
const Modes = {
text: "text",
@@ -135,15 +136,16 @@ const Send: FC<IProps> = ({
const placeholder = `${t("send_to")} ${ChatPrefixes[context]}${name} `;
const members =
context == "channel" ? (channelsData[id]?.is_public ? uids : channelsData[id]?.members) : [];
const isMarkdownMode = mode == Modes.markdown;
return (
<StyledSend
className={`send dark:bg-[#4D5761] ${mode} ${markdownFullscreen ? "fullscreen" : ""} ${replying_mid ? "reply" : ""
} ${context}`}
<div
className={clsx(`send relative bg-gray-200 rounded-lg w-full dark:bg-[#4D5761] ${mode} ${markdownFullscreen ? "fullscreen" : ""} ${replying_mid ? "reply" : ""
} ${context}`, isMarkdownMode && markdownFullscreen && '-mt-9')}
>
{replying_mid && <Replying context={context} mid={replying_mid} id={id} />}
{mode == Modes.text && <UploadFileList context={context} id={id} />}
<div className={`send_box ${mode}`}>
<div className={clsx(`flex justify-between items-start gap-4 px-4 py-3.5`, isMarkdownMode && `grid grid-cols-[1fr_1fr] grid-rows-[auto_auto] gap-0`)}>
<EmojiPicker selectEmoji={insertEmoji} />
{mode == Modes.text && (
<MixedInput
@@ -174,7 +176,7 @@ const Send: FC<IProps> = ({
/>
)}
</div>
</StyledSend>
</div>
);
};
-37
View File
@@ -1,37 +0,0 @@
import styled from "styled-components";
const StyledSend = styled.div`
position: relative;
background: #e5e7eb;
border-radius: var(--br);
width: 100%;
width: -webkit-fill-available;
&.markdown.fullscreen {
margin-top: -35px;
}
.send_box {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 15px;
padding: 14px 18px;
&.markdown {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
gap: 0;
.input {
grid-column: span 2;
}
}
&.reply {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.input {
width: 100%;
}
}
`;
export default StyledSend;
+24 -61
View File
@@ -1,63 +1,26 @@
import styled from "styled-components";
const StyledButton = styled.button`
cursor: pointer;
padding: 10px 14px;
border: none;
box-sizing: border-box;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
border-radius: var(--br, 4px);
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #fff;
background-color: #22ccee;
word-break: keep-all;
&.flex {
width: 100%;
}
&:hover,
&:active {
background-color: #06aed4;
}
&:focus {
background-color: #22ccee;
}
&:disabled {
background-color: #d0d5dd;
}
&.small {
padding: 8px 14px;
font-size: 14px;
line-height: 20px;
}
&.mini {
padding: 4px 10px;
font-size: 12px;
line-height: 18px;
}
&.danger {
border: none;
background-color: #ef4444;
color: #fff;
&:disabled {
background-color: #fecdca;
}
}
&.ghost {
border: 1px solid #1fe1f9;
background: none;
color: #1fe1f9;
}
&.border_less {
box-shadow: none;
border: none !important;
}
&.cancel {
border: 1px solid #e5e7eb;
background: none;
color: #374151;
}
`;
import clsx from "clsx";
import { ButtonHTMLAttributes, ReactNode } from "react";
type Props = ButtonHTMLAttributes<HTMLButtonElement> & { children?: ReactNode }
const StyledButton = ({ children, className = '', ...rest }: Props) => {
const isGhost = className.includes('ghost');
const noBorder = className.includes('border_less');
const isCancel = className.includes('cancel');
const isDanger = className.includes('danger');
const isSmall = className.includes('small');
const isMini = className.includes('mini');
const isFull = className.includes('flex');
return <button className={clsx(`text-sm text-white bg-primary-400 break-keep shadow rounded-lg box-border px-3.5 py-2.5 hover:bg-primary-500 active:bg-primary-500 disabled:bg-gray-300`,
isFull && "w-full",
isGhost && "text-primary-400 border border-primary-400 !bg-transparent",
isCancel && "!bg-transparent text-gray-800 dark:text-gray-50 border border-gray-200",
isSmall && "!py-2",
noBorder && "!shadow-none border-none",
isMini && "!px-2.5 !py-1 !text-xs",
isDanger && "bg-red-700 disabled:bg-gray-300 hover:bg-red-700/80",
className
)} {...rest}>
{children}
</button>;
};
export default StyledButton;
-78
View File
@@ -1,78 +0,0 @@
import styled from "styled-components";
const StyledMenu = styled.ul`
display: flex;
flex-direction: column;
gap: 2px;
padding: 4px;
background-color: #fff;
box-shadow: 0 20px 25px 20px rgba(31, 41, 55, 0.1), 0 10px 10px rgba(31, 41, 55, 0.04);
border-radius: 12px;
min-width: 200px;
.item {
position: relative;
display: flex;
align-items: center;
gap: 14px;
white-space: nowrap;
cursor: pointer;
border-radius: 6px;
padding: 6px;
font-style: normal;
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #616161;
.icon {
width: 20px;
height: 20px;
path {
fill: #475467;
}
}
&.sb {
justify-content: space-between;
}
&:hover {
background-color: #22ccee;
color: #fff;
.icon {
path {
fill: #fff;
}
}
}
&.bottom_line {
margin-bottom: 9px;
&:before {
position: absolute;
content: "";
left: 6px;
bottom: -4px;
display: block;
padding: 0 6px;
box-sizing: border-box;
width: calc(100% - 12px);
height: 1px;
background-color: #f2f4f7;
}
}
&.danger {
color: #a11043;
&:hover {
background-color: #b42318;
color: #fff;
}
}
&[data-disabled="true"] {
color: #a4a8b3;
.icon {
path {
fill: #a4a8b3;
}
}
}
}
`;
export default StyledMenu;
+10 -68
View File
@@ -1,67 +1,5 @@
import { useState, useId, FC } from "react";
import styled from "styled-components";
const StyledForm = styled.form`
width: 100%;
> .option {
&:not(:last-child) {
margin-bottom: 8px;
}
> input[type="radio"] {
display: none;
& + .box {
background: #ffffff;
border: 1px solid #d0d5dd;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
border-radius: 8px;
transition: all ease-in-out 250ms;
& > label {
display: flex;
flex-direction: row;
align-items: center;
font-weight: 400;
font-size: 16px;
line-height: 24px;
color: #667085;
cursor: pointer;
user-select: none;
transition: all ease-in-out 250ms;
&:before {
content: "";
display: inline-block;
width: 14px;
height: 14px;
border-radius: 8px;
background: #ffffff;
box-shadow: inset 0 0 0 4px #ffffff;
border: 1px solid #d0d5dd;
margin: 14px 8px 14px 14px;
transition: all ease-in-out 500ms;
}
}
}
&:checked + .box {
background: #22ccee;
border: 1px solid #d0d5dd;
& > label {
color: #ffffff;
&:before {
background: #ffffff;
box-shadow: inset 0 0 0 4px #22ccee;
border: 1px solid #ffffff;
}
}
}
}
}
`;
type Props = {
options: string[];
values: (string | number)[];
@@ -84,12 +22,12 @@ const Radio: FC<Props> = ({
const [fallbackValue, setFallbackValue] = useState(defaultValue);
const _value = value !== VALUE_NOT_SET ? value : fallbackValue;
return (
<StyledForm>
<form className="w-full flex flex-col gap-2">
{options.map((item, index) => (
<div className="option" key={index}>
<div className="relative bg-transparent" key={index}>
<input
className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer peer z-50"
type="radio"
checked={(values !== VALUES_NOT_SET ? values.indexOf(_value) : _value) === index}
onChange={() => {
@@ -105,12 +43,16 @@ const Radio: FC<Props> = ({
}}
id={`${id}-${index}`}
/>
<div className="box">
<label htmlFor={`${id}-${index}`}>{item}</label>
<div className="drop-shadow px-2 py-3 border border-solid border-[#d0d5dd] rounded-lg w-full h-full bg-white peer-checked:bg-primary-400 text-sm text-gray-500 peer-checked:text-white">
<label className="ml-6" htmlFor={`${id}-${index}`}>{item}</label>
</div>
<div className="absolute top-1/2 left-3 -translate-y-1/2 w-3.5 h-3.5 rounded-full border border-gray-400 peer-checked:hidden"></div>
<div className="absolute top-1/2 left-3 -translate-y-1/2 w-3.5 h-3.5 rounded-full border border-white invisible peer-checked:visible flex-center">
<div className="w-2 h-2 bg-white rounded-full"></div>
</div>
</div>
))}
</StyledForm>
</form>
);
};
export default Radio;
+2 -3
View File
@@ -2,7 +2,6 @@ import { FC, useState } from "react";
import Tippy from "@tippyjs/react";
import IconSelect from "../../../assets/icons/check.sign.svg";
import IconArrow from "../../../assets/icons/arrow.down.svg";
import Menu from "./Menu";
import { useTranslation } from "react-i18next";
@@ -42,7 +41,7 @@ const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }
placement="bottom"
interactive
content={
<Menu>
<ul className="context-menu">
{options.map(({ title, value, selected, underline }) => {
return (
<li
@@ -56,7 +55,7 @@ const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }
</li>
);
})}
</Menu>
</ul>
}
>
<div className="select-none border border-solid border-slate-200 p-2 flex items-center gap-2" onClick={toggleVisible}>
+11 -35
View File
@@ -1,36 +1,12 @@
import styled from "styled-components";
const StyledToggle = styled.div`
cursor: pointer;
position: relative;
width: 44px;
height: 24px;
background-color: #1fe1f9;
border-radius: 12px;
transition: all 0.2s ease-in;
&:after {
border-radius: 50%;
background-color: #fff;
content: "";
display: block;
width: 20px;
height: 20px;
position: absolute;
top: 2px;
right: 2px;
transition: all 0.4s ease;
}
&[data-checked="false"] {
background-color: #f2f4f7;
&:after {
transform: translateX(-100%);
}
}
&[data-disabled="true"] {
cursor: not-allowed;
background-color: #ccc;
pointer-events: none;
}
`;
import clsx from "clsx";
import { HTMLAttributes } from "react";
const StyledToggle = (props: Pick<HTMLAttributes<HTMLDivElement>, "onClick"> & { checked?: boolean, disabled?: boolean }) => {
const { checked = true, disabled = false } = props;
return <div
{...props}
className={clsx(`cursor-pointer relative w-11 h-6 rounded-xl`, checked ? 'bg-primary-400' : 'bg-gray-300', disabled && "cursor-not-allowed bg-gray-400 pointer-events-none")}
>
<div className={clsx("rounded-full bg-white w-5 h-5 absolute top-0.5 right-0.5 transition-all", !checked && "-translate-x-full")}></div>
</div>;
};
export default StyledToggle;