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
+2 -2
View File
@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.821 6.5H19.75C20.8867 6.5 21.8266 7.34297 21.9785 8.43788L21.9948 8.59595L22 8.75V17.75C22 18.9409 21.0748 19.9156 19.904 19.9948L19.75 20H4.25C3.05914 20 2.08436 19.0748 2.00519 17.904L2 17.75V10.499L8.20693 10.5L8.40335 10.4914C8.79396 10.4572 9.16896 10.3214 9.49094 10.0977L9.64734 9.9785L13.821 6.5ZM8.20693 4C8.66749 4 9.1153 4.14129 9.49094 4.40235L9.64734 4.5215L11.75 6.273L8.68706 8.82617L8.60221 8.88738C8.51363 8.94232 8.41452 8.9782 8.31129 8.9927L8.20693 9L2 8.999V6.25C2 5.05914 2.92516 4.08436 4.09595 4.00519L4.25 4H8.20693Z" fill="#70707B"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#70707B" xmlns="http://www.w3.org/2000/svg">
<path d="M13.821 6.5H19.75C20.8867 6.5 21.8266 7.34297 21.9785 8.43788L21.9948 8.59595L22 8.75V17.75C22 18.9409 21.0748 19.9156 19.904 19.9948L19.75 20H4.25C3.05914 20 2.08436 19.0748 2.00519 17.904L2 17.75V10.499L8.20693 10.5L8.40335 10.4914C8.79396 10.4572 9.16896 10.3214 9.49094 10.0977L9.64734 9.9785L13.821 6.5ZM8.20693 4C8.66749 4 9.1153 4.14129 9.49094 4.40235L9.64734 4.5215L11.75 6.273L8.68706 8.82617L8.60221 8.88738C8.51363 8.94232 8.41452 8.9782 8.31129 8.9927L8.20693 9L2 8.999V6.25C2 5.05914 2.92516 4.08436 4.09595 4.00519L4.25 4H8.20693Z" />
</svg>

Before

Width:  |  Height:  |  Size: 676 B

After

Width:  |  Height:  |  Size: 665 B

+2 -2
View File
@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.7541 13.9999C18.9961 13.9999 20.0029 15.0068 20.0029 16.2488V17.1673C20.0029 17.7406 19.8237 18.2997 19.4903 18.7662C17.9445 20.9294 15.4202 22.0011 11.9999 22.0011C8.57891 22.0011 6.05595 20.9289 4.51379 18.7646C4.18182 18.2987 4.00342 17.7409 4.00342 17.1688V16.2488C4.00342 15.0068 5.01027 13.9999 6.25229 13.9999H17.7541ZM11.9999 2.00464C14.7613 2.00464 16.9999 4.24321 16.9999 7.00464C16.9999 9.76606 14.7613 12.0046 11.9999 12.0046C9.23845 12.0046 6.99988 9.76606 6.99988 7.00464C6.99988 4.24321 9.23845 2.00464 11.9999 2.00464Z" fill="#70707B"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="#70707B" xmlns="http://www.w3.org/2000/svg">
<path d="M17.7541 13.9999C18.9961 13.9999 20.0029 15.0068 20.0029 16.2488V17.1673C20.0029 17.7406 19.8237 18.2997 19.4903 18.7662C17.9445 20.9294 15.4202 22.0011 11.9999 22.0011C8.57891 22.0011 6.05595 20.9289 4.51379 18.7646C4.18182 18.2987 4.00342 17.7409 4.00342 17.1688V16.2488C4.00342 15.0068 5.01027 13.9999 6.25229 13.9999H17.7541ZM11.9999 2.00464C14.7613 2.00464 16.9999 4.24321 16.9999 7.00464C16.9999 9.76606 14.7613 12.0046 11.9999 12.0046C9.23845 12.0046 6.99988 9.76606 6.99988 7.00464C6.99988 4.24321 9.23845 2.00464 11.9999 2.00464Z" />
</svg>

Before

Width:  |  Height:  |  Size: 669 B

After

Width:  |  Height:  |  Size: 658 B

+143 -1
View File
@@ -30,8 +30,9 @@
.md-editor {
position: relative;
width: 100%;
width: -webkit-fill-available;
/* width: -webkit-fill-available; */
margin-top: 16px;
grid-column: span 2;
}
.md-editor > div {
transition: height 0.5s ease 0s;
@@ -63,6 +64,147 @@
bottom: 15px;
right: 15px;
}
/* setting container */
.setting-container {
position: relative;
width: 512px;
height: 100%;
display: flex;
flex-direction: column;
gap: 24px;
}
.setting-container .inputs {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 32px;
}
.setting-container .inputs .input {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.setting-container .inputs .input .row {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.setting-container .inputs .input .row.inputs {
flex-direction: column;
gap: 8px;
}
.setting-container .inputs .input .row .title {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.setting-container .inputs .input .row .title .txt {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.setting-container .inputs .input .row .title .txt .icon {
cursor: pointer;
}
.setting-container .inputs .input .row .title .desc {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: #667085;
}
.setting-container .tip {
display: flex;
gap: 8px;
align-items: center;
}
.setting-container .tip .link {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #06b6d4;
}
/* context menu */
.context-menu {
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;
}
.context-menu .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;
}
.context-menu .item .icon {
width: 20px;
height: 20px;
}
.context-menu .item .icon path {
fill: #475467;
}
.context-menu .item.sb {
justify-content: space-between;
}
.context-menu .item:hover {
background-color: #22ccee;
color: #fff;
}
.context-menu .item:hover .icon path {
fill: #fff;
}
.context-menu .item.bottom_line {
margin-bottom: 9px;
}
.context-menu .item.bottom_line:before {
position: absolute;
content: "";
left: 6px;
bottom: -4px;
display: block;
padding: 0 6px;
box-sizing: border-box;
width: calc(88%);
height: 1px;
background-color: #f2f4f7;
}
.context-menu .item.danger {
color: #a11043;
}
.context-menu .item.danger:hover {
background-color: #b42318;
color: #fff;
}
.context-menu .item[data-disabled="true"] {
color: #a4a8b3;
}
.context-menu .item[data-disabled="true"] .icon path {
fill: #a4a8b3;
}
/* text fill transparent */
.text-fill-transparent {
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-fill-color: transparent;
}
}
/* 全局设置滚动条 */
::-webkit-scrollbar-thumb {
+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;
+11 -2
View File
@@ -17,10 +17,19 @@ import NewVersion from "./common/component/NewVersion";
import './i18n';
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
// dark mode
const isDarkMode = localStorage.theme === 'dark';
const isLightMode = localStorage.theme === 'light';
if (isDarkMode || (!isLightMode && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
root.render(
<Suspense fallback="loading">
<Toaster />
<Toaster toastOptions={{
className: "dark:!bg-gray-800 dark:!text-gray-50"
}} />
<DndProvider backend={HTML5Backend}>
<ReduxRoutes />
</DndProvider>
+12 -13
View File
@@ -21,7 +21,6 @@ import IconPin from "../../../assets/icons/pin.svg";
import IconHeadphone from "../../../assets/icons/headphone.svg";
import addIcon from "../../../assets/icons/add.svg?url";
import { StyledUsers, StyledChannelChat } from "./styled";
import InviteModal from "../../../common/component/InviteModal";
import LoadMore from "../LoadMore";
import { useAppSelector } from "../../../app/store";
@@ -151,11 +150,11 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
</header>
}
users={
<StyledUsers className={membersVisible ? "flex" : "hidden"}>
<div className={`flex-col gap-1 w-[226px] h-[calc(100vh_-_56px_-_22px)] overflow-y-scroll p-2 shadow-[inset_1px_0px_0px_rgba(0,_0,_0,_0.1)] ${membersVisible ? "flex" : "hidden"}`}>
{addVisible && (
<div className="add" onClick={toggleAddVisible}>
<img className="icon" src={addIcon} />
<div className="txt dark:!text-gray-50">{t("add_channel_members")}</div>
<div className="cursor-pointer flex items-center justify-start gap-1 select-none rounded-lg p-2.5 hover:bg-gray-500/10" onClick={toggleAddVisible}>
<img className="w-6 h-6" src={addIcon} />
<div className="font-semibold text-sm text-gray-600 dark:text-gray-50">{t("add_channel_members")}</div>
</div>
)}
{memberIds.map((uid: number) => {
@@ -171,19 +170,19 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
/>
);
})}
</StyledUsers>
</div>
}
>
<StyledChannelChat id={`VOCECHAT_FEED_channel_${cid}`}>
<article className="py-4.5 px-4 w-full h-full overflow-x-hidden overflow-y-auto" id={`VOCECHAT_FEED_channel_${cid}`}>
{hasMore ? (
<LoadMore pullUp={pullUp} pulling={pulling} />
) : (
<div className="info">
<h2 className="title dark:text-white">{t("welcome_channel", { name })}</h2>
<p className="desc dark:!text-gray-300">{t("welcome_desc", { name })} </p>
<div className="pt-14 flex flex-col items-start gap-2">
<h2 className="font-bold text-4xl dark:text-white">{t("welcome_channel", { name })}</h2>
<p className="text-gray-600 dark:text-gray-300">{t("welcome_desc", { name })} </p>
{loginUser?.is_admin && (
<NavLink to={`/setting/channel/${cid}?f=${pathname}`} className="edit">
<EditIcon className="icon" />
<NavLink to={`/setting/channel/${cid}?f=${pathname}`} className="flex items-center gap-1 bg-clip-text text-fill-transparent bg-gradient-to-r from-[#3c8ce7] to-primary-400 ">
<EditIcon className="w-4 h-4 fill-[#3c8ce7]" />
{t("edit_channel")}
</NavLink>
)}
@@ -205,7 +204,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
context: "channel"
});
})}
</StyledChannelChat>
</article>
</Layout>
</>
);
-85
View File
@@ -1,85 +0,0 @@
import styled from "styled-components";
export const StyledUsers = styled.div`
/* display: flex; */
flex-direction: column;
gap: 5px;
/* todo */
width: 226px;
height: calc(100vh - 56px - 22px);
overflow-y: scroll;
padding: 8px;
box-shadow: inset 1px 0px 0px rgba(0, 0, 0, 0.1);
> .add {
cursor: pointer;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 4px;
padding: 10px;
border-radius: 8px;
user-select: none;
&:hover {
background: rgba(116, 127, 141, 0.1);
}
.icon {
width: 24px;
height: 24px;
}
.txt {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #52525b;
}
}
`;
export const StyledChannelChat = styled.article`
padding: 18px 16px;
width: 100%;
height: 100%;
height: -webkit-fill-available;
overflow-x: hidden;
overflow-y: auto;
overflow-anchor: auto;
> .info {
padding-top: 62px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
.title {
font-weight: bold;
font-size: 36px;
line-height: 44px;
}
.desc {
color: #78787c;
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
.edit {
display: flex;
align-items: center;
gap: 4px;
.icon {
width: 16px;
height: 16px;
path {
fill: #3c8ce7;
}
}
padding: 0;
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 24px;
background: linear-gradient(135deg, #3c8ce7 0%, #00eaff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
}
`;
+5 -2
View File
@@ -13,6 +13,7 @@ import useLicense from "../../../common/hook/useLicense";
import LicenseUpgradeTip from "./LicenseOutdatedTip";
// import { useTranslation } from "react-i18next";
import DnDTip from "./DnDTip";
import clsx from "clsx";
interface Props {
readonly?: boolean;
@@ -112,13 +113,15 @@ const Layout: FC<Props> = ({
<main className="main" ref={messagesContainer}>
<div className="chat">
{children}
<div className={`send ${selects ? "selecting" : ""}`}>
<div className={`p-4 pt-0 ${selects ? "selecting" : ""}`}>
{readonly ? (
<LoginTip />
) : reachLimit ? (
<LicenseUpgradeTip />
) : (
<Send key={to} id={to} context={context} />
<div className={clsx(`flex justify-center`, selects && "hidden")}>
<Send key={to} id={to} context={context} />
</div>
)}
{selects && <Operations context={context} id={to} />}
</div>
+1 -14
View File
@@ -24,15 +24,6 @@ const Styled = styled.article`
flex-direction: column;
padding: 0;
height: calc(100vh - 56px - 18px);
> .send {
padding: 0 16px 16px 16px;
&.selecting {
padding: 0;
> .send {
display: none;
}
}
}
}
.members {
box-shadow: inset 0px 10px 2px -10px rgba(0, 0, 0, 0.1);
@@ -52,8 +43,7 @@ const Styled = styled.article`
height: 1px;
margin: 18px auto;
}
.tools,
.apps {
.tools {
display: flex;
flex-direction: column;
}
@@ -93,9 +83,6 @@ const Styled = styled.article`
}
}
}
.apps {
gap: 15px;
}
}
}
+35 -23
View File
@@ -2,7 +2,6 @@ import { memo } from "react";
import { Outlet, NavLink, useLocation, useMatch } from "react-router-dom";
import { useTranslation } from "react-i18next";
import StyledWrapper from "./styled";
import User from "./User";
import Loading from "../../common/component/Loading";
import Menu from "./Menu";
@@ -52,49 +51,62 @@ function HomePage() {
// 有点绕
const chatNav = isChatHomePath ? "/chat" : chatPath || "/chat";
const userNav = userPath || "/users";
const linkClass = `flex items-center gap-2.5 px-3 py-2 font-semibold text-sm text-gray-600 rounded-lg hover:bg-black/10`;
return (
<>
<Manifest />
{!guest && <Notification />}
<StyledWrapper className={`dark:!bg-[#121926] ${guest ? "guest" : ""}`}>
<div className={`flex w-screen h-screen dark:bg-[#121926]`}>
{!guest && (
<div className={`col left`}>
<div className={`h-full flex flex-col items-center relative w-16 transition-all`}>
{loginUid && <User uid={loginUid} />}
<nav className="link_navs">
<nav className="flex flex-col gap-1 px-3 py-6">
<NavLink
className={() => {
return `link ${isChattingPage ? "active" : ""}`;
return `${linkClass} ${isChattingPage ? "bg-primary-400" : ""}`;
}}
to={chatNav}
>
<Tooltip tip={t("chat")}>
<ChatIcon />
</Tooltip>
{({ isActive }) => {
return <Tooltip tip={t("chat")}>
<ChatIcon className={isActive ? "fill-white" : ""} />
</Tooltip>;
}}
</NavLink>
<NavLink className="link" to={userNav}>
<Tooltip tip={t("members")}>
<UserIcon />
</Tooltip>
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400' : ""}`} to={userNav}>
{({ isActive }) => {
return <Tooltip tip={t("members")}>
<UserIcon className={isActive ? "fill-white" : ""} />
</Tooltip>;
}}
</NavLink>
<NavLink className="link" to={"/favs"}>
<Tooltip tip={t("favs")}>
<FavIcon />
</Tooltip>
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400' : ""}`} to={"/favs"}>
{({ isActive }) => {
return <Tooltip tip={t("favs")}>
<FavIcon className={isActive ? "fill-white" : ""} />
</Tooltip>;
}}
</NavLink>
<NavLink className="link" to={"/files"}>
<Tooltip tip={t("files")}>
<FolderIcon />
</Tooltip>
<NavLink className={({ isActive }) => `${linkClass} ${isActive ? 'bg-primary-400' : ""}`} to={"/files"}>
{({ isActive }) => {
return <Tooltip tip={t("files")}>
<FolderIcon className={isActive ? "fill-white" : ""} />
</Tooltip>;
}}
</NavLink>
</nav>
<Menu />
</div>
)}
<div className="col right">
<div className="h-full flex flex-col w-full">
<Outlet />
</div>
</StyledWrapper>
</div>
</>
);
}
-66
View File
@@ -1,66 +0,0 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
display: flex;
width: 100vw;
height: 100vh;
background: var(---navs-bg);
&.guest {
background: none;
}
> .col {
height: 100%;
display: flex;
flex-direction: column;
&.left {
align-items: center;
position: relative;
background: transparent;
width: 64px;
transition: all 0.5s ease-in;
> .divider {
width: -webkit-fill-available;
height: 1px;
background-color: #d4d4d4;
margin: 8px 16px;
}
&.expand {
width: 140px;
}
}
&.right {
width: 100%;
}
.link_navs {
display: flex;
flex-direction: column;
gap: 4px;
padding: 24px 12px;
.link {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
padding: 8px 12px;
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #4b5563;
border-radius: 8px;
&:hover {
background-color: rgba(0, 0, 0, 0.08);
}
&.active {
background-color: #55c7ec;
svg path {
fill: #fff;
}
}
}
}
}
&.guest > .col.right {
margin: 0 8px;
}
`;
export default StyledWrapper;
+1 -1
View File
@@ -37,7 +37,7 @@ export default function APIConfig() {
<div className="max-w-[500px] flex flex-col gap-4 items-start">
<Toggle
onClick={handleToggle.bind(null, { third_party: !thirdParty })}
data-checked={thirdParty}
checked={thirdParty}
/>
<div className="w-full flex flex-col items-start gap-2">
<label htmlFor="secret" className="text-sm text-gray-500 dark:text-gray-100"> {t("third_app.key")}:</label>
+1 -1
View File
@@ -41,7 +41,7 @@ const LogoutConfirmModal: FC<Props> = ({ closeModal }) => {
description={t("logout.desc")}
buttons={
<>
<Button onClick={closeModal}>{ct("action.cancel")}</Button>
<Button className="cancel" onClick={closeModal}>{ct("action.cancel")}</Button>
<Button onClick={handleLogout} className="danger">
{exiting ? "Logging out" : ct("action.logout")}
</Button>
+2 -2
View File
@@ -7,7 +7,7 @@ import useConfig from "../../../common/hook/useConfig";
import Server from './server';
import Language from './Language';
import FrontendURL from "./FrontendURL";
// import DarkMode from "./DarkMode";
import DarkMode from "./DarkMode";
import ServerVersionChecker from "../../../common/component/ServerVersionChecker";
export default function Overview() {
@@ -67,7 +67,7 @@ export default function Overview() {
</>
)}
<Language />
{/* <DarkMode /> */}
<DarkMode />
</div>
);
}
+1 -1
View File
@@ -66,7 +66,7 @@ const ProfileBasicEditModal: FC<Props> = ({
}
>
<form ref={formRef} className="flex flex-col gap-2 w-full" action="/">
<label htmlFor={valueKey} className="text-sm text-[#6b7280]">{label}</label>
<label htmlFor={valueKey} className="text-sm text-left text-gray-500">{label}</label>
<Input name={valueKey} value={input} onChange={handleChange} type={type} required></Input>
</form>
</StyledModal>
+3 -4
View File
@@ -1,4 +1,3 @@
import StyledContainer from "./StyledContainer";
import Input from "../../../common/component/styled/Input";
import Textarea from "../../../common/component/styled/Textarea";
import Label from "../../../common/component/styled/Label";
@@ -35,11 +34,11 @@ export default function ConfigAgora() {
} = values as AgoraConfig;
return (
<StyledContainer>
<div className="setting-container">
<div className="inputs">
<div className="input row">
<Label>Enable</Label>
<Toggle onClick={toggleEnable} data-checked={enabled}></Toggle>
<Toggle onClick={toggleEnable} checked={enabled}></Toggle>
</div>
<div className="input">
<Label htmlFor="url">Agora Url</Label>
@@ -111,6 +110,6 @@ export default function ConfigAgora() {
</div>
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={reset} />}
{/* <button onClick={handleUpdate} className="btn">update</button> */}
</StyledContainer>
</div>
);
}
+2 -3
View File
@@ -1,5 +1,4 @@
import { ChangeEvent } from "react";
import StyledContainer from "./StyledContainer";
import Input from "../../../common/component/styled/Input";
import Textarea from "../../../common/component/styled/Textarea";
import Label from "../../../common/component/styled/Label";
@@ -78,7 +77,7 @@ export default function ConfigFirebase() {
client_email,
} = values as FirebaseConfig;
return (
<StyledContainer>
<div className="setting-container">
<StyledRadio
options={Object.values(Options)}
values={Object.keys(Options)}
@@ -131,6 +130,6 @@ export default function ConfigFirebase() {
</fieldset>
}
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={handleReset} />}
</StyledContainer>
</div>
);
}
@@ -57,7 +57,7 @@ const IssuerList: FC<Props> = ({ issuers = [], onChange }) => {
</div>
<div className="w-14 flex justify-end">
<Toggle
data-checked={enable}
checked={enable}
onClick={() => {
onChange(
issuers.map((issuer) => ({
+7 -8
View File
@@ -1,6 +1,5 @@
import { ChangeEvent } from "react";
import toast from "react-hot-toast";
import StyledContainer from "./StyledContainer";
import Toggle from "../../../common/component/styled/Toggle";
import Label from "../../../common/component/styled/Label";
import Input from "../../../common/component/styled/Input";
@@ -71,7 +70,7 @@ export default function Logins() {
const valuesChanged = clientIdChanged || changed || githubChanged;
return (
<StyledContainer>
<div className="setting-container">
<div className="inputs">
<div className="input">
<div className="row">
@@ -83,7 +82,7 @@ export default function Logins() {
</div>
<Toggle
onClick={handleToggle.bind(null, { password: !password })}
data-checked={password}
checked={password}
></Toggle>
</div>
</div>
@@ -97,7 +96,7 @@ export default function Logins() {
</div>
<Toggle
onClick={handleToggle.bind(null, { magic_link: !magic_link })}
data-checked={magic_link}
checked={magic_link}
></Toggle>
</div>
</div>
@@ -112,7 +111,7 @@ export default function Logins() {
</div>
<Toggle
onClick={handleToggle.bind(null, { google: !google })}
data-checked={google}
checked={google}
></Toggle>
</div>
<div className="row">
@@ -135,7 +134,7 @@ export default function Logins() {
</div>
<Toggle
onClick={handleToggle.bind(null, { github: !github })}
data-checked={github}
checked={github}
></Toggle>
</div>
<div className="row inputs">
@@ -166,7 +165,7 @@ export default function Logins() {
</div>
<Toggle
onClick={handleToggle.bind(null, { metamask: !metamask })}
data-checked={metamask}
checked={metamask}
></Toggle>
</div>
</div>
@@ -194,6 +193,6 @@ export default function Logins() {
</div>
</div>
{valuesChanged && <SaveTip saveHandler={handleUpdate} resetHandler={reset} />}
</StyledContainer>
</div>
);
}
+5 -6
View File
@@ -1,17 +1,16 @@
import { useState, useEffect, ChangeEvent } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useSendTestEmailMutation } from "../../../app/services/server";
import iconQuestion from "../../../assets/icons/question.svg?url";
import useConfig from "../../../common/hook/useConfig";
import StyledContainer from "./StyledContainer";
import Input from "../../../common/component/styled/Input";
import Button from "../../../common/component/styled/Button";
import Toggle from "../../../common/component/styled/Toggle";
import Label from "../../../common/component/styled/Label";
import SaveTip from "../../../common/component/SaveTip";
import toast from "react-hot-toast";
import { SMTPConfig } from "../../../types/server";
import { useTranslation } from "react-i18next";
export default function ConfigSMTP() {
const { t } = useTranslation("setting", { keyPrefix: "smtp" });
@@ -54,11 +53,11 @@ export default function ConfigSMTP() {
if (!values) return null;
const { host, port, from, username, password, enabled = false } = values as SMTPConfig;
return (
<StyledContainer>
<div className="setting-container">
<div className="inputs">
<div className="input row">
<Label className="dark:text-gray-200">{t("enable")}</Label>
<Toggle onClick={toggleEnable} data-checked={enabled}></Toggle>
<Toggle onClick={toggleEnable} checked={enabled}></Toggle>
</div>
<div className="input">
<Label className="dark:text-gray-200" htmlFor="name">{t("host")}</Label>
@@ -144,6 +143,6 @@ export default function ConfigSMTP() {
</div>
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={reset} />}
{/* <button onClick={handleUpdate} className="btn">update</button> */}
</StyledContainer>
</div>
);
}
@@ -1,67 +0,0 @@
import styled from "styled-components";
const StyledContainer = styled.div`
position: relative;
width: 512px;
height: 100%;
display: flex;
flex-direction: column;
gap: 24px;
.inputs {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 32px;
.input {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
.row {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
&.inputs {
flex-direction: column;
gap: 8px;
}
.title {
display: flex;
flex-direction: column;
align-items: flex-start;
.txt {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
.icon {
cursor: pointer;
}
}
.desc {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: #667085;
}
}
}
}
}
.tip {
display: flex;
gap: 8px;
align-items: center;
.link {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #06b6d4;
}
}
`;
export default StyledContainer;
+8 -8
View File
@@ -6,7 +6,6 @@ import Search from "./Search";
import User from "../../common/component/User";
import Profile from "../../common/component/Profile";
import StyledWrapper from "./styled";
import BlankPlaceholder from "../../common/component/BlankPlaceholder";
import useFilteredUsers from "../../common/hook/useFilteredUsers";
@@ -24,15 +23,16 @@ function UsersPage() {
}, [pathname]);
if (!users) return null;
return (
<StyledWrapper>
<div className="left dark:!bg-[#1F2A37]">
<div className="flex h-full pt-2 pr-12 pb-2.5">
<div className="rounded-l-2xl bg-white dark:bg-[#1F2A37] relative flex flex-col min-w-[268px] shadow-[inset_-1px_0px_0px_rgba(0,_0,_0,_0.1)]">
<Search input={input} updateInput={updateInput} />
<div className="list">
<nav className="nav">
<div className="px-2 py-3 overflow-scroll pb-[50px]">
<nav className="flex flex-col gap-1">
{users.map(({ uid }) => {
return (
<NavLink key={uid} className="session" to={`/users/${uid}`}>
<NavLink key={uid} className={({ isActive }) => `rounded-md hover:bg-gray-500/10 ${isActive ? "bg-gray-500/10" : ""}`} to={`/users/${uid}`}>
<User uid={uid} enableContextMenu={true} />
</NavLink>
);
@@ -40,10 +40,10 @@ function UsersPage() {
</nav>
</div>
</div>
<div className={`right ${!user_id ? "placeholder" : ""} dark:!bg-[#384250]`}>
<div className={`rounded-r-2xl bg-white w-full flex justify-center items-start ${!user_id ? "h-full items-center" : ""} dark:bg-[#384250]`}>
{user_id ? <Profile uid={+user_id} /> : <BlankPlaceholder type="user" />}
</div>
</StyledWrapper>
</div>
);
}
export default memo(UsersPage);
-50
View File
@@ -1,50 +0,0 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
display: flex;
height: 100%;
padding: 8px 48px 10px 0;
> .left {
border-radius: 16px 0 0 16px;
background-color: #fff;
position: relative;
display: flex;
flex-direction: column;
min-width: 268px;
box-shadow: inset -1px 0px 0px rgba(0, 0, 0, 0.1);
.list {
padding: 12px 8px;
overflow: scroll;
padding-bottom: 50px;
> .nav {
display: flex;
flex-direction: column;
gap: 4px;
a {
border-radius: 6px;
text-decoration: none;
}
.session {
&:hover,
&.active {
background: rgba(116, 127, 141, 0.1);
}
}
}
}
}
.right {
border-radius: 0 16px 16px 0;
background-color: #fff;
/* height: 100vh; */
width: 100%;
display: flex;
justify-content: center;
align-items: flex-start;
&.placeholder {
height: 100%;
align-items: center;
}
}
`;
export default StyledWrapper;