refactor: more tailwind

This commit is contained in:
Tristan Yang
2023-02-02 11:20:27 +08:00
parent 8160c31b39
commit 87d042bf6a
45 changed files with 280 additions and 1308 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "vocechat-web", "name": "vocechat-web",
"version": "0.3.38", "version": "0.3.39",
"private": true, "private": true,
"homepage": "https://voce.chat", "homepage": "https://voce.chat",
"dependencies": { "dependencies": {
+2 -2
View File
@@ -1,3 +1,3 @@
<svg width="14" height="15" viewBox="0 0 14 15" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="15" viewBox="0 0 14 15" fill="black" xmlns="http://www.w3.org/2000/svg">
<path d="M7 6.7929L10.5355 3.25736C10.7308 3.0621 11.0474 3.0621 11.2426 3.25736C11.4379 3.45262 11.4379 3.76921 11.2426 3.96447L7.70711 7.5L11.2426 11.0355C11.4379 11.2308 11.4379 11.5474 11.2426 11.7426C11.0474 11.9379 10.7308 11.9379 10.5355 11.7426L7 8.20711L3.46447 11.7426C3.26921 11.9379 2.95262 11.9379 2.75736 11.7426C2.5621 11.5474 2.5621 11.2308 2.75736 11.0355L6.2929 7.5L2.75736 3.96447C2.5621 3.76921 2.5621 3.45262 2.75736 3.25736C2.95262 3.0621 3.26921 3.0621 3.46447 3.25736L7 6.7929Z" fill="black" fill-opacity="0.5"/> <path d="M7 6.7929L10.5355 3.25736C10.7308 3.0621 11.0474 3.0621 11.2426 3.25736C11.4379 3.45262 11.4379 3.76921 11.2426 3.96447L7.70711 7.5L11.2426 11.0355C11.4379 11.2308 11.4379 11.5474 11.2426 11.7426C11.0474 11.9379 10.7308 11.9379 10.5355 11.7426L7 8.20711L3.46447 11.7426C3.26921 11.9379 2.95262 11.9379 2.75736 11.7426C2.5621 11.5474 2.5621 11.2308 2.75736 11.0355L6.2929 7.5L2.75736 3.96447C2.5621 3.76921 2.5621 3.45262 2.75736 3.25736C2.95262 3.0621 3.26921 3.0621 3.46447 3.25736L7 6.7929Z" fill-opacity="0.8"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 640 B

After

Width:  |  Height:  |  Size: 629 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"> <svg width="24" height="24" viewBox="0 0 24 24" fill="#fff" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5303 11.7204L16.4462 11.6478C16.1526 11.4299 15.7359 11.4541 15.4697 11.7204L12.7503 14.4397L12.75 7.75003L12.7432 7.64826C12.6935 7.28218 12.3797 7.00003 12 7.00003L11.8982 7.00688C11.5322 7.05654 11.25 7.37033 11.25 7.75003L11.2503 14.4417L8.52961 11.7198L8.4455 11.6472C8.15193 11.4293 7.73527 11.4534 7.46895 11.7196C7.176 12.0124 7.17591 12.4873 7.46875 12.7803L11.4687 16.7818L11.5529 16.8544C11.8465 17.0724 12.2632 17.0482 12.5295 16.7819L16.5303 12.7811L16.6029 12.6969C16.8208 12.4033 16.7966 11.9867 16.5303 11.7204ZM6.25 3C4.45507 3 3 4.45507 3 6.25V17.75C3 19.5449 4.45507 21 6.25 21H17.75C19.5449 21 21 19.5449 21 17.75V6.25C21 4.45507 19.5449 3 17.75 3H6.25ZM4.5 6.25C4.5 5.2835 5.2835 4.5 6.25 4.5H17.75C18.7165 4.5 19.5 5.2835 19.5 6.25V17.75C19.5 18.7165 18.7165 19.5 17.75 19.5H6.25C5.2835 19.5 4.5 18.7165 4.5 17.75V6.25Z" fill="#fff"/> <path d="M16.5303 11.7204L16.4462 11.6478C16.1526 11.4299 15.7359 11.4541 15.4697 11.7204L12.7503 14.4397L12.75 7.75003L12.7432 7.64826C12.6935 7.28218 12.3797 7.00003 12 7.00003L11.8982 7.00688C11.5322 7.05654 11.25 7.37033 11.25 7.75003L11.2503 14.4417L8.52961 11.7198L8.4455 11.6472C8.15193 11.4293 7.73527 11.4534 7.46895 11.7196C7.176 12.0124 7.17591 12.4873 7.46875 12.7803L11.4687 16.7818L11.5529 16.8544C11.8465 17.0724 12.2632 17.0482 12.5295 16.7819L16.5303 12.7811L16.6029 12.6969C16.8208 12.4033 16.7966 11.9867 16.5303 11.7204ZM6.25 3C4.45507 3 3 4.45507 3 6.25V17.75C3 19.5449 4.45507 21 6.25 21H17.75C19.5449 21 21 19.5449 21 17.75V6.25C21 4.45507 19.5449 3 17.75 3H6.25ZM4.5 6.25C4.5 5.2835 5.2835 4.5 6.25 4.5H17.75C18.7165 4.5 19.5 5.2835 19.5 6.25V17.75C19.5 18.7165 18.7165 19.5 17.75 19.5H6.25C5.2835 19.5 4.5 18.7165 4.5 17.75V6.25Z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 973 B

After

Width:  |  Height:  |  Size: 962 B

+7
View File
@@ -47,3 +47,10 @@
---channel-text-color: #1c1c1e; ---channel-text-color: #1c1c1e;
---msg-text-color: #374151; ---msg-text-color: #374151;
} }
/* overrides */
#MARKDOWN_CONTAINER .toastui-editor-contents {
display: flex;
flex-direction: column;
align-items: flex-start;
}
+1 -1
View File
@@ -34,7 +34,7 @@ const Avatar: FC<Props> = ({
} else { } else {
return ( return (
<div <div
className="rounded-full flex-center" className={`rounded-full flex-center ${rest.className || ""}`}
style={{ style={{
width, width,
height, height,
+9 -68
View File
@@ -1,69 +1,8 @@
import { ChangeEvent, FC, useState } from "react"; import { ChangeEvent, FC, useState } from "react";
import styled from "styled-components";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import uploadIcon from "../../assets/icons/upload.image.svg?url"; import uploadIcon from "../../assets/icons/upload.image.svg?url";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import clsx from "clsx";
const StyledWrapper = styled.div<{ size: number }>`
width: ${({ size }) => `${size}px`};
height: ${({ size }) => `${size}px`};
position: relative;
cursor: pointer;
.avatar {
overflow: hidden;
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #eee;
img {
object-fit: cover;
width: 100%;
height: 100%;
}
input[type="file"] {
cursor: pointer;
display: block;
opacity: 0;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.tip {
white-space: nowrap;
padding: 4px;
display: none;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
font-weight: bold;
font-size: 12px;
line-height: 18px;
}
&:hover .tip {
display: flex;
}
}
.icon {
display: none;
width: 28px;
height: 28px;
position: absolute;
top: 0;
right: 0;
}
&:hover .icon {
display: block;
}
`;
type UID = number; type UID = number;
interface Props { interface Props {
@@ -102,16 +41,18 @@ const AvatarUploader: FC<Props> = ({
setUploading(false); setUploading(false);
}; };
return ( return (
<StyledWrapper size={size} className={className}> <div style={{ width: `${size}px`, height: `${size}px` }} className={clsx(className, "relative group")}>
<div className="avatar"> <div className="group overflow-hidden relative w-full h-full rounded-full bg-gray-50">
<Avatar width={size} height={size} type={type} src={url} name={name} className={className} /> <Avatar width={size} height={size} type={type} src={url} name={name} className={`${className} object-cover w-full h-full`} />
{!disabled && ( {!disabled && (
<> <>
<div className="tip"> <div className="flex-center flex-col whitespace-nowrap hidden group-hover:flex p-1 absolute top-0 left-0 right-0 bottom-0 bg-black/50 text-white font-bold text-xs">
{uploading ? t("status.uploading") : t("action.change_avatar")} {uploading ? t("status.uploading") : t("action.change_avatar")}
</div> </div>
<input <input
className="opacity-0 absolute top-0 left-0 right-0 bottom-0 block cursor-pointer"
multiple={false} multiple={false}
onChange={handleUpload} onChange={handleUpload}
type="file" type="file"
@@ -122,8 +63,8 @@ const AvatarUploader: FC<Props> = ({
</> </>
)} )}
</div> </div>
{!disabled && <img src={uploadIcon} alt="icon" className="icon" />} {!disabled && <img src={uploadIcon} alt="icon" className="hidden w-7 h-7 absolute top-0 right-0 group-hover:block" />}
</StyledWrapper> </div>
); );
}; };
+2 -2
View File
@@ -14,7 +14,7 @@ interface Props {
type?: "chat" | "user"; type?: "chat" | "user";
} }
const classes = { const classes = {
box: "w-[200px] h-[200px] cursor-pointer bg-[#f9fafb] rounded-3xl flex-center flex-col gap-4", box: "w-[150px] md:w-[200px] h-[150px] md:h-[200px] cursor-pointer bg-[#f9fafb] rounded-3xl flex-center flex-col gap-4",
boxIcon: "w-10 h-10", boxIcon: "w-10 h-10",
boxTip: "px-5 text-sm text-[#475467] font-bold text-center" boxTip: "px-5 text-sm text-[#475467] font-bold text-center"
}; };
@@ -45,7 +45,7 @@ const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => {
{t("desc")} {t("desc")}
</p> </p>
</div> </div>
<div className="grid grid-cols-2 grid-rows-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 grid-rows-2 gap-2 md:gap-6 mt-auto">
<div className={classes.box} onClick={toggleInviteModalVisible}> <div className={classes.box} onClick={toggleInviteModalVisible}>
<IconInvite className={classes.boxIcon} /> <IconInvite className={classes.boxIcon} />
<div className={classes.boxTip}>{t("invite")}</div> <div className={classes.boxTip}>{t("invite")}</div>
+9 -50
View File
@@ -1,50 +1,8 @@
import { FC } from "react"; import { FC } from "react";
import styled from "styled-components";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import { useAppSelector } from "../../app/store"; import { useAppSelector } from "../../app/store";
import clsx from "clsx";
const StyledWrapper = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
padding: 8px;
border-radius: 8px;
user-select: none;
&.compact {
padding: 0;
}
&.interactive {
&:hover,
&.active {
background: rgba(116, 127, 141, 0.1);
}
}
.avatar {
cursor: pointer;
width: ${({ size }: { size: number }) => `${size}px`};
height: ${({ size }: { size: number }) => `${size}px`};
position: relative;
img {
border-radius: 50%;
width: 100%;
height: 100%;
}
}
.name {
display: flex;
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #52525b;
.txt {
max-width: 140px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
`;
interface Props { interface Props {
interactive?: boolean; interactive?: boolean;
@@ -63,12 +21,12 @@ const Channel: FC<Props> = ({ interactive = true, id, compact = false, avatarSiz
if (!channel) return null; if (!channel) return null;
const { name, members = [], is_public, icon } = channel; const { name, members = [], is_public, icon } = channel;
return ( return (
<StyledWrapper <div
size={avatarSize} className={clsx(`flex items-center justify-start gap-2 p-2 rounded-lg select-none`, compact && "p-0", interactive && "hover:bg-gray-500/10")}
className={`${interactive ? "interactive" : ""} ${compact ? "compact" : ""}`}
> >
<div className="avatar"> <div className={`cursor-pointer relative`} style={{ width: `${avatarSize}px`, height: `${avatarSize}px` }}>
<Avatar <Avatar
width={avatarSize} width={avatarSize}
height={avatarSize} height={avatarSize}
@@ -76,14 +34,15 @@ const Channel: FC<Props> = ({ interactive = true, id, compact = false, avatarSiz
src={icon} src={icon}
name={"#"} name={"#"}
alt="avatar" alt="avatar"
className="!w-full !h-full rounded-full"
/> />
</div> </div>
{!compact && ( {!compact && (
<div className="name"> <div className="flex text-sm text-gray-500 font-semibold">
<span className="txt">{name}</span> ({is_public ? totalMemberCount : members.length}) <span className="max-w-[140px] whitespace-nowrap overflow-hidden text-ellipsis">{name}</span> ({is_public ? totalMemberCount : members.length})
</div> </div>
)} )}
</StyledWrapper> </div>
); );
}; };
+1 -12
View File
@@ -1,15 +1,4 @@
import { useState, useEffect, FC } from "react"; import { useState, useEffect, FC } from "react";
import styled from "styled-components";
const Styled = styled.div`
background-color: #fff;
height: 218px;
padding: 15px 15px 0 15px;
line-height: 1.4;
overflow: scroll;
white-space: pre-wrap;
word-break: break-all;
`;
interface Props { interface Props {
url: string; url: string;
@@ -28,7 +17,7 @@ const Doc: FC<Props> = ({ url }) => {
}, [url]); }, [url]);
if (!content) return null; if (!content) return null;
return <Styled>{content}</Styled>; return <div className="bg-white h-[218px] p-[15px] pb-0 whitespace-pre-wrap break-all">{content}</div>;
}; };
export default Doc; export default Doc;
+15 -15
View File
@@ -1,6 +1,5 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import Styled from "./styled";
import ImageMessage from "./ImageMessage"; import ImageMessage from "./ImageMessage";
import useRemoveLocalMessage from "../../hook/useRemoveLocalMessage"; import useRemoveLocalMessage from "../../hook/useRemoveLocalMessage";
import useUploadFile from "../../hook/useUploadFile"; import useUploadFile from "../../hook/useUploadFile";
@@ -12,6 +11,7 @@ import IconDownload from "../../../assets/icons/download.svg";
import IconClose from "../../../assets/icons/close.circle.svg"; import IconClose from "../../../assets/icons/close.circle.svg";
import VideoMessage from "./VideoMessage"; import VideoMessage from "./VideoMessage";
import AudioMessage from "./AudioMessage"; import AudioMessage from "./AudioMessage";
import clsx from "clsx";
const isLocalFile = (content: string) => { const isLocalFile = (content: string) => {
return content.startsWith("blob:"); return content.startsWith("blob:");
@@ -117,7 +117,7 @@ const FileMessage: FC<Props> = ({
removeLocalMessage(properties.local_id); removeLocalMessage(properties.local_id);
}; };
if (!properties) return null; if (!properties) return null;
const icon = getFileIcon(content_type, name); const icon = getFileIcon(content_type, name, "w-9 h-auto");
if (!content || !name) return null; if (!content || !name) return null;
@@ -156,21 +156,21 @@ const FileMessage: FC<Props> = ({
/> />
); );
return ( return (
<Styled className={`file_message ${sending ? "sending" : ""}`}> <div className={clsx(`file_message bg-slate-50 border border-solid border-gray-300 box-border w-[370px] h-[66px] rounded-md`, sending && "opacity-90")}>
<div className="basic"> <div className="p-2 flex items-center justify-between gap-2">
{icon} {icon}
<div className="info"> <div className="flex flex-col gap-1 w-full overflow-hidden">
<span className="name">{name}</span> <span className="font-semibold text-sm text-gray-800 whitespace-nowrap text-ellipsis">{name}</span>
<span className="details"> <span className="whitespace-nowrap text-xs text-gray-500 flex gap-4">
{sending ? ( {sending ? (
<Progress value={progress} width={"80%"} /> <Progress value={progress} width={"80%"} />
) : ( ) : (
<> <>
<i className="size">{formatBytes(size)}</i> <i>{formatBytes(size)}</i>
<i className="time">{dayjs(created_at).fromNow()}</i> <i>{dayjs(created_at).fromNow()}</i>
{fromUser && ( {fromUser && (
<i className="from"> <i>
by <strong>{fromUser.name}</strong> by <strong className="font-bold">{fromUser.name}</strong>
</i> </i>
)} )}
</> </>
@@ -178,14 +178,14 @@ const FileMessage: FC<Props> = ({
</span> </span>
</div> </div>
{sending ? ( {sending ? (
<IconClose className="cancel" onClick={handleCancel} /> <IconClose className="cursor-pointer" onClick={handleCancel} />
) : ( ) : (
<a className="download" download={name} href={`${content}&download=true`}> <a className="whitespace-nowrap" download={name} href={`${content}&download=true`}>
<IconDownload /> <IconDownload className="fill-gray-500" />
</a> </a>
)} )}
</div> </div>
</Styled> </div>
); );
}; };
@@ -1,66 +0,0 @@
import styled from "styled-components";
const Styled = styled.div`
background: #f3f4f6;
border: 1px solid #d4d4d4;
box-sizing: border-box;
border-radius: 6px;
width: 370px;
height: 66px;
&.sending {
opacity: 0.9;
}
* {
user-select: text;
}
.basic {
padding: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
.icon {
width: 36px;
height: 48px;
}
.info {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
overflow: hidden;
.name {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #1c1c1e;
white-space: nowrap;
text-overflow: ellipsis;
}
.details {
white-space: nowrap;
font-weight: 400;
font-size: 12px;
line-height: 18px;
color: #616161;
display: flex;
gap: 16px;
.from strong {
font-weight: bold;
}
}
}
.download {
white-space: nowrap;
svg path{
fill: #616161;
}
}
.cancel {
cursor: pointer;
}
}
`;
export default Styled;
@@ -5,7 +5,6 @@ import Button from "../styled/Button";
import Input from "../styled/Input"; import Input from "../styled/Input";
import Channel from "../Channel"; import Channel from "../Channel";
import User from "../User"; import User from "../User";
// import Channel from "../Channel";
import Reply from "../Message/Reply"; import Reply from "../Message/Reply";
import StyledWrapper from "./styled"; import StyledWrapper from "./styled";
import useForwardMessage from "../../hook/useForwardMessage"; import useForwardMessage from "../../hook/useForwardMessage";
+3 -18
View File
@@ -1,23 +1,8 @@
import { FC, useEffect } from "react"; import { FC, useEffect } from "react";
import IconGithub from "../../assets/icons/github.svg"; import IconGithub from "../../assets/icons/github.svg";
import styled from "styled-components";
import Button from "./styled/Button"; import Button from "./styled/Button";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const StyledSocialButton = styled(Button)`
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
color: #344054;
border: 1px solid #d0d5dd;
background: none !important;
.icon {
width: 24px;
height: 24px;
}
`;
type Props = { type Props = {
source?: "widget" | "webapp", source?: "widget" | "webapp",
@@ -60,10 +45,10 @@ const GithubLoginButton: FC<Props> = ({ type = "login", source = "webapp", clien
}; };
return ( return (
<StyledSocialButton onClick={handleGithubLogin}> <Button className="flex-center gap-3 ghost !text-gray-600 !border-[#d0d5dd]" onClick={handleGithubLogin}>
<IconGithub className="icon" /> <IconGithub className="w-6 h-6" />
{` ${type === "login" ? t("login.github") : t("reg.github")}`} {` ${type === "login" ? t("login.github") : t("reg.github")}`}
</StyledSocialButton> </Button>
); );
}; };
export default GithubLoginButton; export default GithubLoginButton;
+5 -47
View File
@@ -1,54 +1,12 @@
import { FC, useEffect, useState } from "react"; import { FC, useEffect, useState } from "react";
import { GoogleLogin, GoogleOAuthProvider } from "@react-oauth/google"; import { GoogleLogin, GoogleOAuthProvider } from "@react-oauth/google";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import styled from "styled-components";
import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config"; import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
import IconGoogle from "../../assets/icons/google.svg"; import IconGoogle from "../../assets/icons/google.svg";
import Button from "./styled/Button"; import Button from "./styled/Button";
import { useLoginMutation } from "../../app/services/auth"; import { useLoginMutation } from "../../app/services/auth";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const StyledSocialButton = styled(Button)`
position: relative;
width: 100%;
background: white !important;
border: 1px solid #d0d5dd;
color: #344054;
height: 46px;
overflow: hidden;
.mask {
background: inherit;
position: absolute;
left: 0;
top: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
z-index: 998;
height: 40px;
.icon {
width: 24px;
height: 24px;
}
}
> .hide {
left: 0;
top: 0;
position: absolute;
width: 100%;
iframe {
width: 100% !important;
}
}
&:hover {
.hide {
opacity: 0;
z-index: 999;
}
}
`;
interface Props { interface Props {
loadError?: boolean; loadError?: boolean;
@@ -86,16 +44,16 @@ const GoogleLoginInner: FC<Props> = ({ type = "login", loaded, loadError }) => {
}, [error]); }, [error]);
return ( return (
<StyledSocialButton disabled={!loaded || isLoading}> <Button className="group relative w-full !bg-white !text-gray-600 !h-[46px] overflow-hidden !border !border-solid !border-[#d0d5dd]" disabled={!loaded || isLoading}>
<div className="mask"> <div className="absolute left-0 top-0 w-full flex-center gap-3 z-[998] h-10 bg-inherit">
<IconGoogle className="icon" /> <IconGoogle className="w-6 h-6" />
{loadError {loadError
? "Script Load Error!" ? "Script Load Error!"
: loaded : loaded
? `${type === "login" ? t("login.google") : t("reg.google")}` ? `${type === "login" ? t("login.google") : t("reg.google")}`
: `Initializing`} : `Initializing`}
</div> </div>
<div className="hide"> <div className="absolute left-0 top-0 w-full group-hover:opacity-0 group-hover:z-[999]">
<GoogleLogin <GoogleLogin
width="360px" width="360px"
onSuccess={(res) => { onSuccess={(res) => {
@@ -107,7 +65,7 @@ const GoogleLoginInner: FC<Props> = ({ type = "login", loaded, loadError }) => {
}} }}
/> />
</div> </div>
</StyledSocialButton> </Button>
); );
}; };
+7 -50
View File
@@ -1,51 +1,8 @@
import { FC } from "react"; import { FC } from "react";
import styled from "styled-components";
import Modal from "../Modal"; import Modal from "../Modal";
import IconClose from "../../../assets/icons/close.svg"; import IconClose from "../../../assets/icons/close.svg";
import Button from "../styled/Button"; import Button from "../styled/Button";
const Styled = styled.div`
position: relative;
margin-top: 15px;
pointer-events: all;
width: 406px;
padding: 16px;
border-radius: 6px;
background: #fff;
box-shadow: 0 25px 50px rgba(31, 41, 55, 0.25);
display: flex;
flex-direction: column;
gap: 12px;
.tip {
display: flex;
flex-direction: column;
gap: 16px;
color: #344054;
.title {
font-weight: 600;
font-size: 16px;
line-height: 24px;
}
.desc {
font-weight: 400;
font-size: 14px;
line-height: 20px;
}
}
.btns {
width: 100%;
display: flex;
justify-content: flex-end;
gap: 16px;
}
.close {
cursor: pointer;
position: absolute;
top: 16px;
right: 16px;
}
`;
interface Props { interface Props {
handleInstall?: () => void; handleInstall?: () => void;
closePrompt?: () => void; closePrompt?: () => void;
@@ -54,13 +11,13 @@ interface Props {
const Prompt: FC<Props> = ({ handleInstall, closePrompt }) => { const Prompt: FC<Props> = ({ handleInstall, closePrompt }) => {
return ( return (
<Modal mask={false}> <Modal mask={false}>
<Styled> <div className="relative pointer-events-auto mt-4 w-[406px] p-4 rounded-md bg-white shadow-md flex flex-col gap-3">
<IconClose className="close" onClick={closePrompt} /> <IconClose className="absolute top-4 right-4 cursor-pointer" onClick={closePrompt} />
<div className="tip"> <div className="flex flex-col gap-4 text-gray-600">
<h2 className="title">Install web app on desktop?</h2> <h2 className="font-semibold">Install web app on desktop?</h2>
<p className="desc">Add to desktop for quick access to this app.</p> <p className="text-sm">Add to desktop for quick access to this app.</p>
</div> </div>
<div className="btns"> <div className="w-full flex justify-end gap-4">
<Button className="ghost cancel small" onClick={closePrompt}> <Button className="ghost cancel small" onClick={closePrompt}>
Cancel Cancel
</Button> </Button>
@@ -68,7 +25,7 @@ const Prompt: FC<Props> = ({ handleInstall, closePrompt }) => {
Install Install
</Button> </Button>
</div> </div>
</Styled> </div>
</Modal> </Modal>
); );
}; };
+3 -14
View File
@@ -3,21 +3,10 @@ import "prismjs/themes/prism.css";
import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css"; import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css";
//@ts-ignore //@ts-ignore
import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js"; import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js";
import { Viewer } from "@toast-ui/react-editor"; import { Viewer } from "@toast-ui/react-editor";
import styled from "styled-components";
import ImagePreviewModal, { PreviewImageData } from "./ImagePreviewModal"; import ImagePreviewModal, { PreviewImageData } from "./ImagePreviewModal";
const Styled = styled.div`
* {
user-select: text;
}
.toastui-editor-contents {
display: flex;
flex-direction: column;
align-items: flex-start;
}
`;
interface IProps { interface IProps {
content: string; content: string;
@@ -60,9 +49,9 @@ const MarkdownRender: FC<IProps> = ({ content }) => {
{previewImage && ( {previewImage && (
<ImagePreviewModal download={false} data={previewImage} closeModal={closePreviewModal} /> <ImagePreviewModal download={false} data={previewImage} closeModal={closePreviewModal} />
)} )}
<Styled ref={mdContainer}> <div ref={mdContainer} id="MARKDOWN_CONTAINER">
<Viewer initialValue={content} plugins={[codeSyntaxHighlight]}></Viewer> <Viewer initialValue={content} plugins={[codeSyntaxHighlight]}></Viewer>
</Styled> </div>
</> </>
); );
}; };
+17 -39
View File
@@ -1,43 +1,18 @@
// import React from 'react'
import Tippy, { TippyProps } from "@tippyjs/react";
import styled from "styled-components";
import { FC } from "react"; import { FC } from "react";
import Tippy, { TippyProps } from "@tippyjs/react";
import clsx from "clsx";
const StyledTip = styled.div`
position: relative; const Triangle: FC<Pick<TippyProps, "placement">> = ({ placement }) => {
background: #fff; if (placement == "left") return null;
padding: 8px 12px; const cls = clsx("w-3 h-3 bg-inherit absolute rounded-[1px] origin-center rotate-45",
font-weight: 500; placement == "right" && "left-0 top-1/2 -translate-x-1/2 -translate-y-1/2",
font-size: 12px; placement == "top" && "left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2",
line-height: 18px; placement == "bottom" && "top-0 left-1/2 -translate-x-1/2 -translate-y-1/2",
color: #1d2939;
border-radius: var(--br); );
box-shadow: 0 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03); return <i className={cls}></i>;
&::after { };
background-color: inherit;
position: absolute;
content: "";
width: 12px;
height: 12px;
border-radius: 1px;
transform-origin: center;
}
&.right::after {
left: 0;
top: 50%;
transform: translate3d(-50%, -50%, 0) rotate(45deg);
}
&.top::after {
left: 50%;
bottom: 0;
transform: translate3d(-50%, 50%, 0) rotate(45deg);
}
&.bottom::after {
top: 0;
left: 50%;
transform: translate3d(-50%, -50%, 0) rotate(45deg);
}
`;
type Props = { type Props = {
tip: string; tip: string;
@@ -52,7 +27,10 @@ const Tooltip: FC<Props> = ({ tip = "", placement = "right", delay = null, child
duration={delay ? defaultDuration : 0} duration={delay ? defaultDuration : 0}
delay={delay ?? [150, 0]} delay={delay ?? [150, 0]}
placement={placement} placement={placement}
content={<StyledTip className={placement}>{tip}</StyledTip>} content={<div className="relative bg-white px-3 py-2 text-xs rounded-lg drop-shadow text-gray-700">
<Triangle placement={placement} />
{tip}
</div>}
{...rest} {...rest}
> >
{children} {children}
+19 -15
View File
@@ -6,7 +6,6 @@ import IconBot from "../../../assets/icons/bot.svg";
import Avatar from "../Avatar"; import Avatar from "../Avatar";
import Profile from "../Profile"; import Profile from "../Profile";
import ContextMenu from "./ContextMenu"; import ContextMenu from "./ContextMenu";
import StyledWrapper from "./styled";
import useContextMenu from "../../hook/useContextMenu"; import useContextMenu from "../../hook/useContextMenu";
import { useAppSelector } from "../../../app/store"; import { useAppSelector } from "../../../app/store";
import clsx from "clsx"; import clsx from "clsx";
@@ -44,6 +43,11 @@ const User: FC<Props> = ({
}; };
if (!curr) return null; if (!curr) return null;
const online = curr.online || curr.uid == loginUid; const online = curr.online || curr.uid == loginUid;
const containerClass = clsx(`relative flex items-center justify-start gap-2 p-2 rounded-lg select-none `, interactive && "hover:bg-gray-500/10", compact && "p-0");
const nameClass = clsx(`text-sm text-gray-500 max-w-[190px] overflow-hidden text-ellipsis font-semibold`);
const statusClass = clsx(`absolute -bottom-0.5 -right-1.5 w-3 h-3 box-content rounded-full border-[2px] border-solid border-white`,
online ? "bg-[#22c55e]" : "bg-[#a1a1aa]",
compact && "w-3.5 h-3.5");
if (!popover) if (!popover)
return ( return (
<ContextMenu <ContextMenu
@@ -53,30 +57,30 @@ const User: FC<Props> = ({
visible={contextMenuVisible} visible={contextMenuVisible}
hide={hideContextMenu} hide={hideContextMenu}
> >
<StyledWrapper <div
size={avatarSize} className={containerClass}
className={`${interactive ? "interactive" : ""} ${compact ? "compact" : ""} relative`}
onDoubleClick={dm ? handleDoubleClick : undefined} onDoubleClick={dm ? handleDoubleClick : undefined}
onContextMenu={enableContextMenu ? handleContextMenuEvent : undefined} onContextMenu={enableContextMenu ? handleContextMenuEvent : undefined}
> >
<div className="avatar"> <div className="cursor-pointer relative" style={{ width: `${avatarSize}px`, height: `${avatarSize}px` }}>
<Avatar <Avatar
className="w-full h-full rounded-full object-cover"
width={avatarSize} width={avatarSize}
height={avatarSize} height={avatarSize}
src={curr.avatar} src={curr.avatar}
name={curr.name} name={curr.name}
alt="avatar" alt="avatar"
/> />
<div className={`status ${online ? "online" : "offline"}`}></div> <div className={statusClass}></div>
</div> </div>
{!compact && ( {!compact && (
<span className="name" title={curr?.name}> <span className={nameClass} title={curr?.name}>
{curr?.name} {curr?.name}
</span> </span>
)} )}
{owner && <IconOwner />} {owner && <IconOwner />}
{curr.is_bot && <IconBot className={clsx(compact && "absolute -top-1 -right-1", "!w-4 !h-4")} />} {curr.is_bot && <IconBot className={clsx(compact && "absolute -top-1 -right-1", "!w-4 !h-4")} />}
</StyledWrapper> </div>
</ContextMenu> </ContextMenu>
); );
return ( return (
@@ -94,30 +98,30 @@ const User: FC<Props> = ({
trigger="click" trigger="click"
content={<Profile uid={uid} type="card" cid={cid} />} content={<Profile uid={uid} type="card" cid={cid} />}
> >
<StyledWrapper <div
size={avatarSize} className={containerClass}
className={`${interactive ? "interactive" : ""} ${compact ? "compact" : ""}`}
onDoubleClick={dm ? handleDoubleClick : undefined} onDoubleClick={dm ? handleDoubleClick : undefined}
onContextMenu={enableContextMenu ? handleContextMenuEvent : undefined} onContextMenu={enableContextMenu ? handleContextMenuEvent : undefined}
> >
<div className="avatar"> <div className="cursor-pointer relative" style={{ width: `${avatarSize}px`, height: `${avatarSize}px` }}>
<Avatar <Avatar
className="w-full h-full rounded-full object-cover"
width={avatarSize} width={avatarSize}
height={avatarSize} height={avatarSize}
src={curr.avatar} src={curr.avatar}
name={curr.name} name={curr.name}
alt="avatar" alt="avatar"
/> />
<div className={`status ${online ? "online" : "offline"}`}></div> <div className={statusClass}></div>
</div> </div>
{!compact && ( {!compact && (
<span className="name" title={curr?.name}> <span className={nameClass} title={curr?.name}>
{curr?.name} {curr?.name}
</span> </span>
)} )}
{owner && <IconOwner />} {owner && <IconOwner />}
{curr.is_bot && <IconBot className="!w-4 !h-4" />} {curr.is_bot && <IconBot className="!w-4 !h-4" />}
</StyledWrapper> </div>
</Tippy> </Tippy>
</ContextMenu> </ContextMenu>
); );
-67
View File
@@ -1,67 +0,0 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
padding: 8px;
border-radius: 8px;
user-select: none;
&.interactive {
&:hover,
&.active {
background: rgba(116, 127, 141, 0.1);
}
}
.avatar {
cursor: pointer;
width: ${({ size }: { size: number }) => `${size}px`};
height: ${({ size }: { size: number }) => `${size}px`};
position: relative;
img {
object-fit: cover;
border-radius: 50%;
width: 100%;
height: 100%;
}
.status {
position: absolute;
bottom: -2px;
right: -6px;
width: 12px;
height: 12px;
box-sizing: content-box;
border-radius: 50%;
border: 2px solid #fff;
&.online {
background-color: #22c55e;
}
&.offline {
background-color: #a1a1aa;
}
&.alert {
background-color: #dc2626;
}
}
}
.name {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #52525b;
max-width: 190px;
overflow: hidden;
text-overflow: ellipsis;
}
/* session nav */
&.compact {
padding: 0;
.avatar .status {
width: 15px;
height: 15px;
}
}
`;
export default StyledWrapper;
+8 -8
View File
@@ -160,7 +160,7 @@ export function sliceFile(file: File | null, chunksAmount: number) {
return chunks; return chunks;
} }
export const getFileIcon = (type: string, name = "") => { export const getFileIcon = (type: string, name = "", className = "icon") => {
let icon = null; let icon = null;
const checks = { const checks = {
@@ -176,27 +176,27 @@ export const getFileIcon = (type: string, name = "") => {
switch (true) { switch (true) {
case checks.image.test(_type): case checks.image.test(_type):
{ {
icon = <IconImage className="icon" />; icon = <IconImage className={className} />;
} }
break; break;
case checks.pdf.test(_type): case checks.pdf.test(_type):
icon = <IconPdf className="icon" />; icon = <IconPdf className={className} />;
break; break;
case checks.code.test(_type): case checks.code.test(_type):
icon = <IconCode className="icon" />; icon = <IconCode className={className} />;
break; break;
case checks.doc.test(_type): case checks.doc.test(_type):
icon = <IconDoc className="icon" />; icon = <IconDoc className={className} />;
break; break;
case checks.audio.test(_type): case checks.audio.test(_type):
icon = <IconAudio className="icon" />; icon = <IconAudio className={className} />;
break; break;
case checks.video.test(_type): case checks.video.test(_type):
icon = <IconVideo className="icon" />; icon = <IconVideo className={className} />;
break; break;
default: default:
icon = <IconUnknown className="icon" />; icon = <IconUnknown className={className} />;
break; break;
} }
return icon; return icon;
+9 -70
View File
@@ -6,74 +6,13 @@ import IconClose from "../../../assets/icons/close.svg";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import PinnedMessage from "../../../common/component/PinnedMessage"; import PinnedMessage from "../../../common/component/PinnedMessage";
const Styled = styled.div` const Styled = styled.div`
padding: 16px; .preview {
background: #f9fafb;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 12px;
min-width: 486px;
max-height: 90vh;
overflow-y: scroll;
/* width: fit-content; */
> .head {
font-weight: 600;
font-size: 16px;
line-height: 24px;
color: #344054;
margin-bottom: 16px;
}
> .none {
padding: 16px;
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
.tip {
width: 240px;
font-weight: 600;
font-size: 16px;
line-height: 24px;
text-align: center;
color: #475467;
}
}
> .list {
display: flex;
flex-direction: column;
gap: 8px;
.pin {
position: relative;
border: 1px solid #f2f4f7;
border-radius: var(--br);
> .preview {
background: none; background: none;
.down img { .down img {
width: 100% !important; width: 100% !important;
height: auto !important; height: auto !important;
} }
} }
> .opts {
visibility: hidden;
display: flex;
align-items: center;
gap: 4px;
position: absolute;
top: 4px;
right: 4px;
padding: 4px;
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 6px;
.btn {
display: flex;
background: none;
border: none;
}
}
&:hover .opts {
visibility: visible;
}
}
}
`; `;
type Props = { type Props = {
id: number; id: number;
@@ -88,22 +27,22 @@ const PinList: FC<Props> = ({ id }: Props) => {
}; };
const noPins = pins.length == 0; const noPins = pins.length == 0;
return ( return (
<Styled> <Styled className="p-4 drop-shadow-md overflow-y-scroll max-h-[90vh] min-w-[486px] rounded-xl bg-gray-50">
<h4 className="head">{t("pinned_msg")}({pins.length})</h4> <h4 className=" text-gray-600 mb-4 font-semibold">{t("pinned_msg")}({pins.length})</h4>
{noPins ? ( {noPins ? (
<div className="none"> <div className="flex flex-col items-center gap-2 w-full p-4">
<IconSurprise /> <IconSurprise />
<div className="tip">{t("pin_empty_tip")}</div> <div className="w-60 font-semibold text-gray-500 text-center">{t("pin_empty_tip")}</div>
</div> </div>
) : ( ) : (
<ul className="list"> <ul className="flex flex-col gap-2">
{pins.map((data) => { {pins.map((data) => {
return ( return (
<li key={data.mid} className="pin"> <li key={data.mid} className="group relative border border-solid border-slate-100 rounded-md ">
<PinnedMessage data={data} /> <PinnedMessage data={data} />
<div className="opts"> <div className="invisible group-hover:visible flex items-center gap-1 absolute top-1 right-1 p-1 border border-solid border-black/10 rounded-md">
{canPin && ( {canPin && (
<button className="btn" data-mid={data.mid} onClick={handleUnpin}> <button className="flex bg-none border-none" data-mid={data.mid} onClick={handleUnpin}>
<IconClose className="fill-slate-900" /> <IconClose className="fill-slate-900" />
</button> </button>
)} )}
+6 -6
View File
@@ -21,7 +21,7 @@ import IconPin from "../../../assets/icons/pin.svg";
import IconHeadphone from "../../../assets/icons/headphone.svg"; import IconHeadphone from "../../../assets/icons/headphone.svg";
import addIcon from "../../../assets/icons/add.svg?url"; import addIcon from "../../../assets/icons/add.svg?url";
import { StyledUsers, StyledChannelChat, StyledHeader } from "./styled"; import { StyledUsers, StyledChannelChat } from "./styled";
import InviteModal from "../../../common/component/InviteModal"; import InviteModal from "../../../common/component/InviteModal";
import LoadMore from "../LoadMore"; import LoadMore from "../LoadMore";
import { useAppSelector } from "../../../app/store"; import { useAppSelector } from "../../../app/store";
@@ -142,13 +142,13 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
</ul> </ul>
} }
header={ header={
<StyledHeader className="head"> <header className="head flex items-center h-full justify-center md:justify-between">
<div className="txt"> <div className="flex items-center gap-1 text-base">
<ChannelIcon personal={!is_public} /> <ChannelIcon personal={!is_public} />
<span className="title">{name}</span> <span className="text-gray-800">{name}</span>
<span className="desc">{description}</span> <span className="ml-2 text-gray-500">{description}</span>
</div> </div>
</StyledHeader> </header>
} }
users={ users={
<StyledUsers className={membersVisible ? "flex" : "hidden"}> <StyledUsers className={membersVisible ? "flex" : "hidden"}>
+1 -23
View File
@@ -1,27 +1,5 @@
import styled from "styled-components"; import styled from "styled-components";
export const StyledHeader = styled.header`
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.txt {
display: flex;
align-items: center;
gap: 5px;
.title {
font-size: 16px;
line-height: 24px;
color: #1c1c1e;
}
.desc {
margin-left: 8px;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #616161;
}
}
`;
export const StyledNotification = styled.div` export const StyledNotification = styled.div`
padding: 3px 8px; padding: 3px 8px;
font-style: normal; font-style: normal;
+4 -5
View File
@@ -7,7 +7,6 @@ import FavIcon from "../../../assets/icons/bookmark.svg";
import { useReadMessageMutation } from "../../../app/services/message"; import { useReadMessageMutation } from "../../../app/services/message";
import User from "../../../common/component/User"; import User from "../../../common/component/User";
import Layout from "../Layout"; import Layout from "../Layout";
import { StyledHeader, StyledDMChat } from "./styled";
import LoadMore from "../LoadMore"; import LoadMore from "../LoadMore";
import { renderMessageFragment } from "../utils"; import { renderMessageFragment } from "../utils";
import useMessageFeed from "../../../common/hook/useMessageFeed"; import useMessageFeed from "../../../common/hook/useMessageFeed";
@@ -65,12 +64,12 @@ const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
</ul> </ul>
} }
header={ header={
<StyledHeader className="head"> <header className="head h-full flex items-center justify-between">
<User interactive={false} uid={currUser.uid} /> <User interactive={false} uid={currUser.uid} />
</StyledHeader> </header>
} }
> >
<StyledDMChat id={`VOCECHAT_FEED_user_${uid}`}> <article id={`VOCECHAT_FEED_user_${uid}`} className="w-full h-full px-4 py-4.5 overflow-auto">
{hasMore ? <LoadMore pullUp={pullUp} pulling={pulling} /> : null} {hasMore ? <LoadMore pullUp={pullUp} pulling={pulling} /> : null}
{[...feeds].map((mid, idx) => { {[...feeds].map((mid, idx) => {
const curr = messageData[mid]; const curr = messageData[mid];
@@ -86,7 +85,7 @@ const DMChat: FC<Props> = ({ uid = 0, dropFiles }) => {
context: "user" context: "user"
}); });
})} })}
</StyledDMChat> </article>
</Layout> </Layout>
); );
}; };
-36
View File
@@ -1,36 +0,0 @@
import styled from "styled-components";
export const StyledHeader = styled.header`
height: 100%;
/* padding: 0 20px 0 10px; */
display: flex;
align-items: center;
justify-content: space-between;
/* tricky */
> div {
padding-left: 4px;
}
.txt {
display: flex;
align-items: center;
gap: 5px;
.title {
font-size: 16px;
line-height: 24px;
color: #1c1c1e;
}
.desc {
margin-left: 8px;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #616161;
}
}
`;
export const StyledDMChat = styled.article`
width: 100%;
padding: 18px 16px;
height: 100%;
height: -webkit-fill-available;
overflow: auto;
`;
+6 -6
View File
@@ -4,7 +4,7 @@ import useMessageFeed from "../../../common/hook/useMessageFeed";
import ChannelIcon from "../../../common/component/ChannelIcon"; import ChannelIcon from "../../../common/component/ChannelIcon";
import Layout from "../Layout"; import Layout from "../Layout";
import { renderMessageFragment } from "../utils"; import { renderMessageFragment } from "../utils";
import { StyledChannelChat, StyledHeader } from "./styled"; import { StyledChannelChat } from "./styled";
import LoadMore from "../LoadMore"; import LoadMore from "../LoadMore";
import { useAppSelector } from "../../../app/store"; import { useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -40,13 +40,13 @@ export default function GuestChannelChat({ cid = 0 }: Props) {
to={cid} to={cid}
context="channel" context="channel"
header={ header={
<StyledHeader className="head"> <header className="head flex items-center h-full justify-center md:justify-between">
<div className="txt"> <div className="flex items-center gap-1 text-base">
<ChannelIcon personal={!is_public} /> <ChannelIcon personal={!is_public} />
<span className="title">{name}</span> <span className="text-gray-800">{name}</span>
<span className="desc">{description}</span> <span className="ml-2 text-gray-500">{description}</span>
</div> </div>
</StyledHeader> </header>
} }
> >
<StyledChannelChat id={`VOCECHAT_FEED_channel_${cid}`}> <StyledChannelChat id={`VOCECHAT_FEED_channel_${cid}`}>
@@ -1,27 +1,4 @@
import styled from "styled-components"; import styled from "styled-components";
export const StyledHeader = styled.header`
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.txt {
display: flex;
align-items: center;
gap: 5px;
.title {
font-size: 16px;
line-height: 24px;
color: #1c1c1e;
}
.desc {
margin-left: 8px;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #616161;
}
}
`;
export const StyledChannelChat = styled.article` export const StyledChannelChat = styled.article`
padding: 18px 16px; padding: 18px 16px;
width: 100%; width: 100%;
+5 -3
View File
@@ -14,6 +14,7 @@ import GuestBlankPlaceholder from "./GuestBlankPlaceholder";
import GuestChannelChat from "./GuestChannelChat"; import GuestChannelChat from "./GuestChannelChat";
import GuestSessionList from "./GuestSessionList"; import GuestSessionList from "./GuestSessionList";
import IconList from '../../assets/icons/list.svg'; import IconList from '../../assets/icons/list.svg';
import IconClose from '../../assets/icons/close.svg';
function ChatPage() { function ChatPage() {
const [sessionListVisible, setSessionListVisible] = useState(false); const [sessionListVisible, setSessionListVisible] = useState(false);
const [channelModalVisible, setChannelModalVisible] = useState(false); const [channelModalVisible, setChannelModalVisible] = useState(false);
@@ -51,13 +52,14 @@ function ChatPage() {
<ChannelModal closeModal={toggleChannelModalVisible} personal={true} /> <ChannelModal closeModal={toggleChannelModalVisible} personal={true} />
)} )}
{usersModalVisible && <UsersModal closeModal={toggleUsersModalVisible} />} {usersModalVisible && <UsersModal closeModal={toggleUsersModalVisible} />}
<StyledWrapper className={`${isGuest ? "!pr-1" : ""} md:!pr-12`}> <StyledWrapper className={`${isGuest ? "!pr-1 !pt-0" : ""} md:!pr-12`}>
{sessionListVisible && <div onClick={toggleSessionList} className="z-30 fixed top-0 left-4 w-screen h-screen bg-black/50 transition-all backdrop-blur-sm"></div>}
<div className={clsx("left !fixed top-0 left-0 z-40 transition-all md:!relative md:translate-x-0 md:overflow-auto", sessionListVisible ? "translate-x-0" : "-translate-x-full")}> <div className={clsx("left !fixed top-0 left-0 z-40 transition-all md:!relative md:translate-x-0 md:overflow-auto", sessionListVisible ? "translate-x-0" : "-translate-x-full")}>
<Server readonly={isGuest} /> <Server readonly={isGuest} />
{isGuest ? <GuestSessionList /> : <SessionList tempSession={tmpSession} />} {isGuest ? <GuestSessionList /> : <SessionList tempSession={tmpSession} />}
<button className="absolute top-1/2 -right-[24px] z-50 p-2 rounded-full bg-slate-300/80 md:hidden" onClick={toggleSessionList}> {sessionListVisible ? null : <button className="absolute top-2 -right-[52px] z-50 p-2 bg-none md:hidden" onClick={toggleSessionList}>
<IconList /> <IconList />
</button> </button>}
</div> </div>
<div className={`right ${placeholderVisible ? "placeholder" : ""}`}> <div className={`right ${placeholderVisible ? "placeholder" : ""}`}>
{placeholderVisible && (isGuest ? <GuestBlankPlaceholder /> : <BlankPlaceholder />)} {placeholderVisible && (isGuest ? <GuestBlankPlaceholder /> : <BlankPlaceholder />)}
+4 -19
View File
@@ -1,23 +1,8 @@
import { FC } from "react"; import { FC } from "react";
import styled from "styled-components";
import { NavLink, useLocation } from "react-router-dom"; import { NavLink, useLocation } from "react-router-dom";
import Avatar from "../../common/component/Avatar"; import Avatar from "../../common/component/Avatar";
import { useAppSelector } from "../../app/store"; import { useAppSelector } from "../../app/store";
const StyledWrapper = styled.div`
padding: 10px 12px;
.avatar {
width: 32px;
height: 32px;
img {
object-fit: cover;
border-radius: 50%;
width: 100%;
height: 100%;
}
}
`;
interface Props { interface Props {
uid: number; uid: number;
} }
@@ -28,13 +13,13 @@ const User: FC<Props> = ({ uid }) => {
if (!user) return null; if (!user) return null;
return ( return (
<StyledWrapper> <div className="px-3 py-2.5 invisible md:visible">
<NavLink to={`/setting?nav=my_account&f=${pathname}`}> <NavLink to={`/setting?nav=my_account&f=${pathname}`}>
<div className="avatar"> <div className="w-8 h-8">
<Avatar width={32} height={32} src={user.avatar} name={user.name} /> <Avatar className=" object-cover w-full h-full" width={32} height={32} src={user.avatar} name={user.name} />
</div> </div>
</NavLink> </NavLink>
</StyledWrapper> </div>
); );
}; };
+1 -7
View File
@@ -1,12 +1,6 @@
import styled from "styled-components";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import Button from "../../common/component/styled/Button"; import Button from "../../common/component/styled/Button";
const StyledMagicButton = styled(Button)`
width: 100%;
margin-bottom: 16px;
`;
export default function MagicLinkLogin() { export default function MagicLinkLogin() {
const navigate = useNavigate(); const navigate = useNavigate();
const handleGoogleLogin = () => { const handleGoogleLogin = () => {
@@ -14,5 +8,5 @@ export default function MagicLinkLogin() {
// signIn(); // signIn();
}; };
return <StyledMagicButton onClick={handleGoogleLogin}>Sign in with Magic Link</StyledMagicButton>; return <Button className="w-full mb-4" onClick={handleGoogleLogin}>Sign in with Magic Link</Button>;
} }
+9 -57
View File
@@ -1,5 +1,4 @@
import { useEffect, useState, FC, useRef } from "react"; import { useEffect, useState, FC, useRef } from "react";
import styled from "styled-components";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import StyledInput from "../../../common/component/styled/Input"; import StyledInput from "../../../common/component/styled/Input";
@@ -15,53 +14,6 @@ import { useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useWizard } from "react-use-wizard"; import { useWizard } from "react-use-wizard";
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .primaryText {
text-align: center;
font-weight: 700;
font-size: 24px;
line-height: 30px;
margin-bottom: 8px;
}
> .secondaryText {
text-align: center;
font-size: 14px;
line-height: 20px;
margin-bottom: 24px;
}
form {
> .input {
margin-bottom: 20px;
width: 360px;
height: 44px;
font-weight: 400;
font-size: 16px;
line-height: 24px;
padding: 10px 14px;
border: 1px solid #d0d5dd;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
> .inner {
padding: 0;
font-weight: 400;
font-size: 16px;
line-height: 24px;
}
}}
> .button {
width: 360px;
margin-top: 24px;
}
`;
type Props = { type Props = {
serverName: string; serverName: string;
}; };
@@ -122,12 +74,12 @@ const AdminAccount: FC<Props> = ({ serverName }) => {
}, [isUpdatedServer]); }, [isUpdatedServer]);
return ( return (
<StyledWrapper> <div className="h-full flex-center flex-col text-center w-[360px] m-auto">
<span className="primaryText">{t("admin_title")}</span> <span className="text-2xl mb-2 font-bold">{t("admin_title")}</span>
<span className="secondaryText">{t("admin_desc")}</span> <span className="text-sm mb-6">{t("admin_desc")}</span>
<form ref={formRef} action="/"> <form ref={formRef} action="/" className="flex flex-col gap-2 w-full">
<StyledInput <StyledInput
className="input" className="large"
placeholder="Enter your email" placeholder="Enter your email"
type={"email"} type={"email"}
required required
@@ -135,7 +87,7 @@ const AdminAccount: FC<Props> = ({ serverName }) => {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
/> />
<StyledInput <StyledInput
className="input" className="large"
type="password" type="password"
required required
minLength={6} minLength={6}
@@ -144,7 +96,7 @@ const AdminAccount: FC<Props> = ({ serverName }) => {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />
<StyledInput <StyledInput
className="input" className="large"
type="password" type="password"
required required
minLength={6} minLength={6}
@@ -154,7 +106,7 @@ const AdminAccount: FC<Props> = ({ serverName }) => {
/> />
</form> </form>
<StyledButton <StyledButton
className="button" className="mt-6 w-full"
onClick={async () => { onClick={async () => {
const formEle = formRef?.current; const formEle = formRef?.current;
if (formEle) { if (formEle) {
@@ -174,7 +126,7 @@ const AdminAccount: FC<Props> = ({ serverName }) => {
> >
{!(isSigningUp || isLoggingIn || isUpdatingServer) ? t("sign") : "..."} {!(isSigningUp || isLoggingIn || isUpdatingServer) ? t("sign") : "..."}
</StyledButton> </StyledButton>
</StyledWrapper> </div>
); );
}; };
export default AdminAccount; export default AdminAccount;
+9 -63
View File
@@ -1,80 +1,26 @@
import styled from "styled-components";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import StyledButton from "../../../common/component/styled/Button"; import StyledButton from "../../../common/component/styled/Button";
import PlayIcon from "../../../assets/icons/play.svg?url"; import PlayIcon from "../../../assets/icons/play.svg?url";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .primaryText {
text-align: center;
font-weight: 700;
font-size: 24px;
line-height: 30px;
margin-bottom: 8px;
}
> .secondaryText {
text-align: center;
font-weight: 400;
font-size: 14px;
line-height: 20px;
margin-bottom: 48px;
}
> .tip {
width: 588px;
font-size: 20px;
line-height: 24px;
text-align: center;
margin-bottom: 48px;
> .strong {
font-weight: 700;
}
}
> .startButton {
width: 128px;
display: flex;
flex-direction: column;
align-items: center;
padding: 15px 0 12px;
> img {
margin-bottom: 7px;
}
> span {
font-weight: 500;
font-size: 14px;
line-height: 20px;
}
}
`;
export default function DonePage({ serverName }: { serverName: string }) { export default function DonePage({ serverName }: { serverName: string }) {
const { t } = useTranslation("welcome", { keyPrefix: "onboarding" }); const { t } = useTranslation("welcome", { keyPrefix: "onboarding" });
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<StyledWrapper> <div className="h-full flex-center flex-col text-center w-[588px] m-auto">
<span className="primaryText">{t("done_welcome", { serverName })}</span> <span className="text-2xl font-bold mb-2">{t("done_welcome", { serverName })}</span>
<span className="secondaryText">{t("done_title")}</span> <span className="text-sm mb-12">{t("done_title")}</span>
<span className="tip"> <span className="text-xl mb-12">
<Trans i18nKey={"onboarding.done_desc"} ns={"welcome"}> <Trans i18nKey={"onboarding.done_desc"} ns={"welcome"}>
<span className="strong" /> <span className="font-bold" />
</Trans> </Trans>
</span> </span>
<StyledButton className="startButton" onClick={() => navigate("/")}> <StyledButton className="!w-32 flex flex-col items-center py-3" onClick={() => navigate("/")}>
<img src={PlayIcon} alt="play icon" /> <img className="mb-2" src={PlayIcon} alt="play icon" />
<span>{t("enter")}</span> <span className="text-sm">{t("enter")}</span>
</StyledButton> </StyledButton>
</StyledWrapper> </div>
); );
} }
+7 -48
View File
@@ -1,52 +1,10 @@
import { FC } from "react"; import { FC } from "react";
import styled from "styled-components";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import StyledInput from "../../../common/component/styled/Input"; import StyledInput from "../../../common/component/styled/Input";
import StyledButton from "../../../common/component/styled/Button"; import StyledButton from "../../../common/component/styled/Button";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useWizard } from "react-use-wizard"; import { useWizard } from "react-use-wizard";
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .primaryText {
text-align: center;
font-weight: 700;
font-size: 24px;
line-height: 30px;
margin-bottom: 8px;
}
> .secondaryText {
width: 360px;
text-align: center;
font-size: 14px;
line-height: 20px;
margin-bottom: 24px;
color: #667085;
}
> .input {
width: 360px;
height: 44px;
font-weight: 400;
font-size: 16px;
line-height: 24px;
padding: 10px 14px;
border: 1px solid #d0d5dd;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
}
> .button {
width: 360px;
margin-top: 24px;
}
`;
type Props = { type Props = {
serverName: string; serverName: string;
setServerName: (name: string) => void; setServerName: (name: string) => void;
@@ -54,20 +12,21 @@ type Props = {
const ServerName: FC<Props> = ({ serverName, setServerName }) => { const ServerName: FC<Props> = ({ serverName, setServerName }) => {
const { t } = useTranslation("welcome", { keyPrefix: "onboarding" }); const { t } = useTranslation("welcome", { keyPrefix: "onboarding" });
const { nextStep } = useWizard(); const { nextStep } = useWizard();
return ( return (
<StyledWrapper> <div className="h-full flex-center flex-col text-center w-[360px] m-auto">
<span className="primaryText">{t("new_server")}</span> <span className="text-2xl mb-2 font-bold">{t("new_server")}</span>
<span className="secondaryText"> <span className="text-sm mb-6 text-gray-400 ">
{t("server_desc")} {t("server_desc")}
</span> </span>
<StyledInput <StyledInput
className="input" className="h-11 px-3.5 py-2.5 border-gray-300 rounded-lg shadow"
placeholder={t("placeholder_server")} placeholder={t("placeholder_server")}
value={serverName} value={serverName}
onChange={(e) => setServerName(e.target.value)} onChange={(e) => setServerName(e.target.value)}
/> />
<StyledButton <StyledButton
className="button" className="w-full mt-6"
onClick={() => { onClick={() => {
// Verification for space name // Verification for space name
if (serverName === "") { if (serverName === "") {
@@ -79,7 +38,7 @@ const ServerName: FC<Props> = ({ serverName, setServerName }) => {
> >
{t("create_server")} {t("create_server")}
</StyledButton> </StyledButton>
</StyledWrapper> </div>
); );
}; };
export default ServerName; export default ServerName;
+5 -47
View File
@@ -1,64 +1,22 @@
import styled from "styled-components";
import StyledButton from "../../../common/component/styled/Button"; import StyledButton from "../../../common/component/styled/Button";
import PlayIcon from "../../../assets/icons/play.svg?url"; import PlayIcon from "../../../assets/icons/play.svg?url";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useWizard } from "react-use-wizard"; import { useWizard } from "react-use-wizard";
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .primaryText {
text-align: center;
font-weight: 700;
font-size: 24px;
line-height: 30px;
margin-bottom: 8px;
}
> .secondaryText {
text-align: center;
font-size: 14px;
line-height: 20px;
margin-bottom: 24px;
}
> .startButton {
width: 128px;
display: flex;
flex-direction: column;
align-items: center;
padding: 15px 0 12px;
> img {
margin-bottom: 7px;
}
> span {
font-weight: 500;
font-size: 14px;
line-height: 20px;
}
}
`;
export default function WelcomePage() { export default function WelcomePage() {
const { t } = useTranslation("welcome", { keyPrefix: "onboarding" }); const { t } = useTranslation("welcome", { keyPrefix: "onboarding" });
const { nextStep } = useWizard(); const { nextStep } = useWizard();
return ( return (
<StyledWrapper> <div className="flex-center flex-col h-full text-center">
<span className="primaryText">{t("welcome")}</span> <span className="text-2xl mb-2 font-bold">{t("welcome")}</span>
<span className="secondaryText"> <span className="text-sm mb-6">
{t("welcome_desc")} {t("welcome_desc")}
</span> </span>
<StyledButton className="startButton" onClick={nextStep}> <StyledButton className="!w-32 flex flex-col gap-2 items-center py-3 text-sm" onClick={nextStep}>
<img src={PlayIcon} alt="play icon" /> <img src={PlayIcon} alt="play icon" />
<span>{t("start")}</span> <span>{t("start")}</span>
</StyledButton> </StyledButton>
</StyledWrapper> </div>
); );
} }
+16 -37
View File
@@ -8,39 +8,6 @@ import { WhoCanSignUp } from "../../../types/server";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useWizard } from "react-use-wizard"; import { useWizard } from "react-use-wizard";
const StyledWrapper = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .primaryText {
text-align: center;
font-weight: 700;
font-size: 24px;
line-height: 30px;
margin-bottom: 8px;
}
> .secondaryText {
text-align: center;
font-size: 14px;
line-height: 20px;
margin-bottom: 24px;
}
> form {
width: 512px;
}
> .button {
width: 124px;
height: 44px;
margin-top: 24px;
}
`;
export default function SignUpSetting() { export default function SignUpSetting() {
const { t } = useTranslation("welcome"); const { t } = useTranslation("welcome");
const { t: st } = useTranslation("setting"); const { t: st } = useTranslation("setting");
@@ -73,10 +40,22 @@ export default function SignUpSetting() {
if (isSuccess) nextStep(); if (isSuccess) nextStep();
}, [isSuccess]); }, [isSuccess]);
const StyledWrapper = styled.div`
/* > form {
width: 512px;
}
> .button {
width: 124px;
height: 44px;
margin-top: 24px;
} */
`;
return ( return (
<StyledWrapper> <StyledWrapper className="h-full flex-center flex-col text-center w-[512px] m-auto">
<span className="primaryText">{t("onboarding.invite_title")}</span> <span className="font-bold text-2xl mb-2">{t("onboarding.invite_title")}</span>
<span className="secondaryText">{t("onboarding.invite_desc")}</span> <span className="text-sm mb-6">{t("onboarding.invite_desc")}</span>
{value && <StyledRadio {value && <StyledRadio
options={[st("overview.sign_up.everyone"), st("overview.sign_up.invite")]} options={[st("overview.sign_up.everyone"), st("overview.sign_up.invite")]}
values={["EveryOne", "InvitationOnly"]} values={["EveryOne", "InvitationOnly"]}
@@ -84,7 +63,7 @@ export default function SignUpSetting() {
onChange={setValue} onChange={setValue}
/>} />}
<StyledButton <StyledButton
className="button" className="w-32 mt-6"
disabled={!value} disabled={!value}
onClick={() => { onClick={() => {
// nextStep(); // nextStep();
+14 -53
View File
@@ -1,4 +1,3 @@
import styled from "styled-components";
import IconPdf from "../../../assets/icons/file.pdf.svg"; import IconPdf from "../../../assets/icons/file.pdf.svg";
import IconAudio from "../../../assets/icons/file.audio.svg"; import IconAudio from "../../../assets/icons/file.audio.svg";
import IconVideo from "../../../assets/icons/file.video.svg"; import IconVideo from "../../../assets/icons/file.video.svg";
@@ -9,73 +8,34 @@ import IconImage from "../../../assets/icons/file.image.svg";
import CheckSign from "../../../assets/icons/check.sign.svg"; import CheckSign from "../../../assets/icons/check.sign.svg";
import { FC } from "react"; import { FC } from "react";
const Styled = styled.div`
padding: 12px;
background: #ffffff;
min-width: 200px;
overflow: auto;
box-shadow: 0 24px 48px -12px rgba(16, 24, 40, 0.18);
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: flex-start;
position: relative;
> .list {
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
.type {
position: relative;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #475467;
.icon {
width: 15px;
height: auto;
}
.check {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}
}
`;
export const FileTypes = { export const FileTypes = {
doc: { doc: {
title: "Documents", title: "Documents",
icon: <IconDoc className="icon" /> icon: <IconDoc className="w-4 h-auto" />
}, },
pdf: { pdf: {
title: "PDFs", title: "PDFs",
icon: <IconPdf className="icon" /> icon: <IconPdf className="w-4 h-auto" />
}, },
image: { image: {
title: "Images", title: "Images",
icon: <IconImage className="icon" /> icon: <IconImage className="w-4 h-auto" />
}, },
audio: { audio: {
title: "Audio", title: "Audio",
icon: <IconAudio className="icon" /> icon: <IconAudio className="w-4 h-auto" />
}, },
video: { video: {
title: "Videos", title: "Videos",
icon: <IconVideo className="icon" /> icon: <IconVideo className="w-4 h-auto" />
}, },
code: { code: {
title: "Code Snippets", title: "Code Snippets",
icon: <IconCode className="icon" /> icon: <IconCode className="w-4 h-auto" />
}, },
unknown: { unknown: {
title: "Unknown Files", title: "Unknown Files",
icon: <IconUnknown className="icon" /> icon: <IconUnknown className="w-4 h-auto" />
} }
}; };
type Props = { type Props = {
@@ -86,23 +46,24 @@ const Type: FC<Props> = ({ select = "", updateFilter }) => {
const handleClick = (type?: string) => { const handleClick = (type?: string) => {
updateFilter({ type }); updateFilter({ type });
}; };
return ( return (
<Styled> <div className="p-3 bg-white min-w-[100px] overflow-auto shadow-md rounded-lg flex flex-col items-start relative">
<ul className="list"> <ul className="w-full flex flex-col gap-4">
<li className="type" onClick={handleClick.bind(null, undefined)}> <li className="flex justify-between" onClick={handleClick.bind(null, undefined)}>
Any Type Any Type
{!select && <CheckSign className="check" />} {!select && <CheckSign className="check" />}
</li> </li>
{Object.entries(FileTypes).map(([type, { title, icon }]) => { {Object.entries(FileTypes).map(([type, { title, icon }]) => {
return ( return (
<li key={title} className="type" onClick={handleClick.bind(null, type)}> <li key={title} className="relative cursor-pointer flex items-center gap-2 text-sm text-gray-500 font-semibold" onClick={handleClick.bind(null, type)}>
{icon} {title} {icon} {title}
{select == type && <CheckSign className="check" />} {select == type && <CheckSign className="absolute right-0 left-1/2 -translate-y-1/2" />}
</li> </li>
); );
})} })}
</ul> </ul>
</Styled> </div>
); );
}; };
export default Type; export default Type;
+3 -3
View File
@@ -10,9 +10,9 @@ interface Props {
const SentTip: FC<Props> = ({ email, reset }) => { const SentTip: FC<Props> = ({ email, reset }) => {
const { t } = useTranslation("auth"); const { t } = useTranslation("auth");
return ( return (
<div className="tips"> <div className="flex flex-col items-center">
<h2 className="title">{t("check_email")}</h2> <h2 className="font-semibold text-2xl text-gray-800 mb-2">{t("check_email")}</h2>
<span className="desc"> <span className="text-center text-gray-500 mb-6">
{t("check_email_desc", { email })} {t("check_email_desc", { email })}
</span> </span>
<Button onClick={reset} className="main flex"> <Button onClick={reset} className="main flex">
+11 -12
View File
@@ -2,13 +2,13 @@ import { useState, useEffect, ChangeEvent, FormEvent } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import BASE_URL from "../../app/config"; import BASE_URL from "../../app/config";
import StyledWrapper from "./styled";
import Input from "../../common/component/styled/Input"; import Input from "../../common/component/styled/Input";
import Button from "../../common/component/styled/Button"; import Button from "../../common/component/styled/Button";
import { useSendLoginMagicLinkMutation } from "../../app/services/auth"; import { useSendLoginMagicLinkMutation } from "../../app/services/auth";
import SentTip from "./SentTip"; import SentTip from "./SentTip";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import SocialLoginButtons from "../login/SocialLoginButtons"; import SocialLoginButtons from "../login/SocialLoginButtons";
import Divider from "../../common/component/Divider";
export default function SendMagicLinkPage() { export default function SendMagicLinkPage() {
const { t } = useTranslation("auth"); const { t } = useTranslation("auth");
@@ -62,20 +62,19 @@ export default function SendMagicLinkPage() {
setEmail(""); setEmail("");
setSent(false); setSent(false);
}; };
return ( return (
<StyledWrapper> <div className="flex-center h-screen">
<div className="form"> <div className="py-8 px-10 shadow rounded-xl">
{sent ? ( {sent ? (
<SentTip email={email} reset={handleReset} /> <SentTip email={email} reset={handleReset} />
) : ( ) : (
<> <>
<div className="tips"> <div className="flex flex-col items-center">
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="logo" /> <img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="w-14 h-14 mb-7 rounded-full" />
<h2 className="title">{t("login.title")}</h2> <h2 className="font-semibold text-2xl text-gray-800 mb-2">{t("login.title")}</h2>
<span className="desc">{t("placeholder_email")}</span> <span className="text-center text-gray-500 mb-6">{t("placeholder_email")}</span>
</div> </div>
<form onSubmit={handleLogin}> <form onSubmit={handleLogin} className="flex flex-col gap-5 w-[360px]">
<Input <Input
type="email" type="email"
className="large" className="large"
@@ -91,14 +90,14 @@ export default function SendMagicLinkPage() {
{isLoading ? "Sending" : t("continue")} {isLoading ? "Sending" : t("continue")}
</Button> </Button>
</form> </form>
<hr className="or" /> <Divider content="or" />
<div className="flex flex-col gap-3 py-3"> <div className="flex flex-col gap-3 py-3">
<SocialLoginButtons /> <SocialLoginButtons />
</div> </div>
<Button onClick={handlePwdPath}>{t("login.password")}</Button> <Button onClick={handlePwdPath} className="flex">{t("login.password")}</Button>
</> </>
)} )}
</div> </div>
</StyledWrapper> </div>
); );
} }
-71
View File
@@ -1,71 +0,0 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
.form {
max-width: 440px;
padding: 32px 40px 32px 40px;
box-shadow: 0 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
border-radius: 12px;
.tips {
display: flex;
flex-direction: column;
align-items: center;
.logo {
width: 56px;
height: 56px;
margin-bottom: 28px;
border-radius: 50%;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 32px;
color: #101828;
margin-bottom: 8px;
}
.desc {
text-align: center;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #667085;
margin-bottom: 24px;
}
}
form {
display: flex;
flex-direction: column;
gap: 20px;
width: 360px;
}
.or {
border: none;
position: relative;
height: 1px;
background-color: #e4e7ec;
margin: 26px 0;
overflow: visible;
&:after {
padding: 4px;
background-color: #fff;
content: "OR";
position: absolute;
left: 50%;
top: 50%;
transform: translate3d(-50%, -50%, 0);
font-size: 14px;
line-height: 20px;
color: #667085;
}
}
button {
width: 100%;
}
}
`;
export default StyledWrapper;
@@ -44,7 +44,7 @@ const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
const { t } = useTranslation("setting"); const { t } = useTranslation("setting");
const { t: ct } = useTranslation(); const { t: ct } = useTranslation();
const [getUrl, { isLoading, isSuccess }] = useGetLicensePaymentUrlMutation(); const [getUrl, { isLoading, isSuccess }] = useGetLicensePaymentUrlMutation();
const [host, setHost] = useState(location.host); const [host, setHost] = useState(location.hostname);
const [popUpVisible, setPopUpVisible] = useState(false); const [popUpVisible, setPopUpVisible] = useState(false);
const [selectPrice, setSelectPrice] = useState( const [selectPrice, setSelectPrice] = useState(
`${LicensePriceList[0].pid}|${LicensePriceList[0].limit}|${LicensePriceList[0].type}|${LicensePriceList[0].sub_dur || ""}` `${LicensePriceList[0].pid}|${LicensePriceList[0].limit}|${LicensePriceList[0].type}|${LicensePriceList[0].sub_dur || ""}`
+5 -25
View File
@@ -1,5 +1,4 @@
import { ChangeEvent, FC, useEffect, useState } from "react"; import { ChangeEvent, FC, useEffect, useState } from "react";
import styled from "styled-components";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import Modal from "../../common/component/Modal"; import Modal from "../../common/component/Modal";
import StyledModal from "../../common/component/styled/Modal"; import StyledModal from "../../common/component/styled/Modal";
@@ -8,25 +7,6 @@ import Checkbox from "../../common/component/styled/Checkbox";
import useLogout from "../../common/hook/useLogout"; import useLogout from "../../common/hook/useLogout";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const StyledConfirm = styled(StyledModal)`
.clear {
font-weight: normal;
font-size: 14px;
line-height: 20px;
color: #6b7280;
display: flex;
justify-content: flex-end;
align-items: center;
.txt {
cursor: pointer;
color: orange;
margin-right: 12px;
}
input {
cursor: pointer;
}
}
`;
interface Props { interface Props {
closeModal: () => void; closeModal: () => void;
@@ -56,7 +36,7 @@ const LogoutConfirmModal: FC<Props> = ({ closeModal }) => {
return ( return (
<Modal id="modal-modal"> <Modal id="modal-modal">
<StyledConfirm <StyledModal
title={t("logout.title")} title={t("logout.title")}
description={t("logout.desc")} description={t("logout.desc")}
buttons={ buttons={
@@ -68,13 +48,13 @@ const LogoutConfirmModal: FC<Props> = ({ closeModal }) => {
</> </>
} }
> >
<div className="clear"> <div className="text-sm text-gray-400 flex justify-end items-center">
<label htmlFor="clear_cb" className="txt"> <label htmlFor="clear_cb" className="cursor-pointer text-orange-500 mr-3">
{t("logout.clear_local")} {t("logout.clear_local")}
</label> </label>
<Checkbox name="clear_cb" checked={clearLocal} onChange={handleCheck} /> <Checkbox className=" cursor-pointer" name="clear_cb" checked={clearLocal} onChange={handleCheck} />
</div> </div>
</StyledConfirm> </StyledModal>
</Modal> </Modal>
); );
}; };
+27 -120
View File
@@ -1,105 +1,14 @@
import { useEffect, useState, MouseEvent } from "react"; import { useEffect, useState, MouseEvent } from "react";
import styled from "styled-components";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { useUpdateAvatarMutation } from "../../app/services/user"; import { useUpdateAvatarMutation } from "../../app/services/user";
import AvatarUploader from "../../common/component/AvatarUploader"; import AvatarUploader from "../../common/component/AvatarUploader";
import Button from "../../common/component/styled/Button";
import ProfileBasicEditModal from "./ProfileBasicEditModal"; import ProfileBasicEditModal from "./ProfileBasicEditModal";
import RemoveAccountConfirmModal from "./RemoveAccountConfirmModal"; import RemoveAccountConfirmModal from "./RemoveAccountConfirmModal";
import UpdatePasswordModal from "./UpdatePasswordModal"; import UpdatePasswordModal from "./UpdatePasswordModal";
import { useAppSelector } from "../../app/store"; import { useAppSelector } from "../../app/store";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 32px;
.card {
padding: 24px;
display: flex;
flex-direction: column;
align-items: center;
width: 512px;
background: #f3f4f6;
border-radius: 20px;
.name {
margin-top: 8px;
margin-bottom: 64px;
font-weight: bold;
font-size: 18px;
line-height: 28px;
color: #27272a;
.uid {
font-weight: normal;
color: #52525b;
}
}
.row {
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: 24px;
.info {
display: flex;
flex-direction: column;
.label {
font-weight: 600;
font-size: 12px;
line-height: 20px;
text-transform: uppercase;
color: #52525b;
}
.txt {
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #52525b;
.gray {
color: #78787c;
}
}
}
.btn {
background: #1fe1f9;
border: 1px solid #1fe1f9;
}
}
}
.danger {
display: flex;
flex-direction: column;
align-items: flex-start;
.head {
font-weight: bold;
font-size: 16px;
line-height: 24px;
color: #374151;
}
.desc {
font-weight: normal;
font-size: 12px;
line-height: 18px;
color: #616161;
margin-bottom: 16px;
}
.btn {
background: #ef4444;
border: 1px solid #ef4444;
}
}
.btn {
color: #fff;
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
padding: 8px 14px;
background: #1fe1f9;
border: 1px solid #1fe1f9;
box-sizing: border-box;
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
border-radius: 8px;
}
`;
type EditField = "name" | "email" | ""; type EditField = "name" | "email" | "";
export default function MyAccount() { export default function MyAccount() {
const { t } = useTranslation("member"); const { t } = useTranslation("member");
@@ -151,51 +60,49 @@ export default function MyAccount() {
const { uid, avatar, name, email } = loginUser; const { uid, avatar, name, email } = loginUser;
return ( return (
<> <>
<StyledWrapper> <div className="flex flex-col items-start gap-8">
<div className="card"> <div className="p-6 flex flex-col items-center w-[512px] bg-slate-100 rounded-2xl">
<AvatarUploader url={avatar} name={name} uploadImage={uploadAvatar} /> <AvatarUploader url={avatar} name={name} uploadImage={uploadAvatar} />
<div className="name"> <div className="mt-2 mb-16 font-bold text-lg text-gray-800">
{name} <span className="uid">#{uid}</span> {name} <span className="font-normal text-gray-500">#{uid}</span>
</div> </div>
<div className="row"> <div className="w-full flex justify-between mb-6">
<div className="info"> <div className="flex flex-col text-gray-500">
<span className="label">{t("username")}</span> <span className="text-xs uppercase font-semibold">{t("username")}</span>
<span className="txt"> <span className="text-sm ">
{name} <span className="gray"> #{uid}</span> {name} <span className="text-gray-600"> #{uid}</span>
</span> </span>
</div> </div>
<button data-edit="name" onClick={handleBasicEdit} className="btn"> <Button data-edit="name" onClick={handleBasicEdit} className="">
{ct("action.edit")} {ct("action.edit")}
</button> </Button>
</div> </div>
<div className="row"> <div className="w-full flex justify-between mb-6">
<div className="info"> <div className="flex flex-col text-gray-500">
<span className="label">{t("email")}</span> <span className="text-xs uppercase font-semibold">{t("email")}</span>
<span className="txt">{email}</span> <span className="text-sm">{email}</span>
</div> </div>
<button data-edit="email" onClick={handleBasicEdit} className="btn"> <Button data-edit="email" onClick={handleBasicEdit}>
{ct("action.edit")} {ct("action.edit")}
</button> </Button>
</div> </div>
<div className="row"> <div className="w-full flex justify-between mb-6">
<div className="info"> <div className="flex flex-col text-gray-500">
<span className="label">{t("password")}</span> <span className="text-xs uppercase font-semibold">{t("password")}</span>
<span className="txt">*********</span> <span className="text-sm">*********</span>
</div> </div>
<button onClick={togglePasswordModal} className="btn"> <Button onClick={togglePasswordModal}>
{ct("action.edit")} {ct("action.edit")}
</button> </Button>
</div> </div>
</div> </div>
{/* uid 1 是初始账户,不能删 */} {/* uid 1 是初始账户,不能删 */}
{uid != 1 && ( {uid != 1 && (
<div className="danger"> <Button className="danger" onClick={toggleRemoveAccountModalVisible}>
<button className="btn" onClick={toggleRemoveAccountModalVisible}>
{t("delete_account")} {t("delete_account")}
</button> </Button>
</div>
)} )}
</StyledWrapper> </div>
{editModal && ( {editModal && (
<ProfileBasicEditModal <ProfileBasicEditModal
type={editModal == "email" ? "email" : "text"} type={editModal == "email" ? "email" : "text"}
+11 -24
View File
@@ -1,5 +1,4 @@
import { ChangeEvent, useEffect, useState, FC } from "react"; import { ChangeEvent, useEffect, useState, FC } from "react";
import styled from "styled-components";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import Input from "../../common/component/styled/Input"; import Input from "../../common/component/styled/Input";
import { useUpdatePasswordMutation, useGetCredentialsQuery } from "../../app/services/auth"; import { useUpdatePasswordMutation, useGetCredentialsQuery } from "../../app/services/auth";
@@ -8,21 +7,6 @@ import Button from "../../common/component/styled/Button";
import Modal from "../../common/component/Modal"; import Modal from "../../common/component/Modal";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const StyledEdit = styled(StyledModal)`
.input {
margin: 16px 0;
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
label {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #6b7280;
}
}
`;
interface Props { interface Props {
closeModal: () => void; closeModal: () => void;
@@ -75,9 +59,12 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
!confirmPassword || !confirmPassword ||
newPassword !== confirmPassword || newPassword !== confirmPassword ||
isLoading; isLoading;
const inputClass = `my-4 w-full flex flex-col gap-2`;
const labelClass = `text-gray-400 font-semibold`;
return ( return (
<Modal id="modal-modal"> <Modal id="modal-modal">
<StyledEdit <StyledModal
title={t("change_pwd")} title={t("change_pwd")}
description={t("change_pwd_desc")} description={t("change_pwd_desc")}
buttons={ buttons={
@@ -92,8 +79,8 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
} }
> >
{data?.password && ( {data?.password && (
<div className="input"> <div className={inputClass}>
<label htmlFor={"current"}>{t("current_pwd")}</label> <label className={labelClass} htmlFor={"current"}>{t("current_pwd")}</label>
<Input <Input
type="password" type="password"
id="current" id="current"
@@ -104,8 +91,8 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
></Input> ></Input>
</div> </div>
)} )}
<div className="input"> <div className={inputClass}>
<label htmlFor={"newPassword"}>{t("new_pwd")}</label> <label className={labelClass} htmlFor={"newPassword"}>{t("new_pwd")}</label>
<Input <Input
type="password" type="password"
name={"newPassword"} name={"newPassword"}
@@ -114,8 +101,8 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
onChange={handleChange} onChange={handleChange}
></Input> ></Input>
</div> </div>
<div className="input"> <div className={inputClass}>
<label htmlFor={"confirmPassword"}>{t("confirm_new_pwd")}</label> <label className={labelClass} htmlFor={"confirmPassword"}>{t("confirm_new_pwd")}</label>
<Input <Input
onBlur={handleCompare} onBlur={handleCompare}
type="password" type="password"
@@ -125,7 +112,7 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
onChange={handleChange} onChange={handleChange}
></Input> ></Input>
</div> </div>
</StyledEdit> </StyledModal>
</Modal> </Modal>
); );
}; };
+10 -40
View File
@@ -1,5 +1,4 @@
import { useState, useEffect, ChangeEvent } from "react"; import { useState, useEffect, ChangeEvent } from "react";
import styled from "styled-components";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { import {
useChangeChannelTypeMutation, useChangeChannelTypeMutation,
@@ -13,41 +12,11 @@ import Label from "../../common/component/styled/Label";
import Radio from "../../common/component/styled/Radio"; import Radio from "../../common/component/styled/Radio";
import Textarea from "../../common/component/styled/Textarea"; import Textarea from "../../common/component/styled/Textarea";
import SaveTip from "../../common/component/SaveTip"; import SaveTip from "../../common/component/SaveTip";
import channelIcon from "../../assets/icons/channel.svg?url"; import IconChannel from "../../assets/icons/channel.svg";
import { useAppSelector } from "../../app/store"; import { useAppSelector } from "../../app/store";
import { Channel } from "../../types/channel"; import { Channel } from "../../types/channel";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const StyledWrapper = 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: 24px;
.input {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
.name {
padding-left: 36px;
background-image: url(${channelIcon});
background-size: 20px;
background-position-x: 8px;
background-position-y: 8px;
background-repeat: no-repeat;
}
}
}
`;
export default function Overview({ id = 0 }) { export default function Overview({ id = 0 }) {
const { t } = useTranslation("setting", { keyPrefix: "channel" }); const { t } = useTranslation("setting", { keyPrefix: "channel" });
const { t: ct } = useTranslation(); const { t: ct } = useTranslation();
@@ -119,16 +88,16 @@ export default function Overview({ id = 0 }) {
if (!values || !id) return null; if (!values || !id) return null;
const { name, description } = values; const { name, description } = values;
const readOnly = !loginUser?.is_admin && channel?.owner != loginUser?.uid; const readOnly = !loginUser?.is_admin && channel?.owner != loginUser?.uid;
const inputClass = `w-full flex flex-col items-start gap-2 relative`;
return ( return (
<StyledWrapper> <div className="relative w-[512px] flex flex-col gap-6 h-full">
<AvatarUploader type="channel" url={channel?.icon} name={name} uploadImage={updateIcon} /> <AvatarUploader type="channel" url={channel?.icon} name={name} uploadImage={updateIcon} />
<div className="inputs"> <div className="flex flex-col gap-6 items-start">
<div className="input"> <div className={inputClass}>
<Label htmlFor="name">{t("name")}</Label> <Label htmlFor="name">{t("name")}</Label>
<Input <Input
disabled={readOnly} disabled={readOnly}
className="name" className="!pl-8"
data-type="name" data-type="name"
onChange={handleChange} onChange={handleChange}
value={name} value={name}
@@ -136,8 +105,9 @@ export default function Overview({ id = 0 }) {
id="name" id="name"
placeholder={t("name")} placeholder={t("name")}
/> />
<IconChannel className="absolute bottom-2.5 left-2" />
</div> </div>
<div className="input"> <div className={inputClass}>
<Label htmlFor="desc">{t("topic")}</Label> <Label htmlFor="desc">{t("topic")}</Label>
<Textarea <Textarea
disabled={readOnly} disabled={readOnly}
@@ -149,7 +119,7 @@ export default function Overview({ id = 0 }) {
id="name" id="name"
placeholder={t("topic_placeholder")} /> placeholder={t("topic_placeholder")} />
</div> </div>
{!readOnly && <div className="input"> {!readOnly && <div className={inputClass}>
<Label htmlFor="desc">{t("visibility")}</Label> <Label htmlFor="desc">{t("visibility")}</Label>
<Radio <Radio
options={[t("public"), t("private")]} options={[t("public"), t("private")]}
@@ -164,6 +134,6 @@ export default function Overview({ id = 0 }) {
</div>} </div>}
</div> </div>
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={handleReset} />} {changed && <SaveTip saveHandler={handleUpdate} resetHandler={handleReset} />}
</StyledWrapper> </div>
); );
} }
+4 -30
View File
@@ -1,4 +1,3 @@
import styled from "styled-components";
import Tippy from "@tippyjs/react"; import Tippy from "@tippyjs/react";
import searchIcon from "../../assets/icons/search.svg?url"; import searchIcon from "../../assets/icons/search.svg?url";
import addIcon from "../../assets/icons/add.svg?url"; import addIcon from "../../assets/icons/add.svg?url";
@@ -6,31 +5,6 @@ import AddEntriesMenu from "../../common/component/AddEntriesMenu";
import Tooltip from "../../common/component/Tooltip"; import Tooltip from "../../common/component/Tooltip";
import { FC, ChangeEvent } from "react"; import { FC, ChangeEvent } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const StyledWrapper = styled.div`
position: relative;
min-height: 56px;
padding: 6px 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
.search {
display: flex;
align-items: center;
gap: 5px;
.input {
width: 100%;
border: none;
outline: none;
background: none;
font-weight: normal;
font-size: 14px;
line-height: 20px;
}
}
`;
type Props = { type Props = {
input: string, input: string,
updateInput: (input: string) => void updateInput: (input: string) => void
@@ -41,17 +15,17 @@ const Search: FC<Props> = ({ input, updateInput }) => {
updateInput(evt.target.value); updateInput(evt.target.value);
}; };
return ( return (
<StyledWrapper> <div className="relative min-h-[56px] px-2 py-3 flex items-center justify-between gap-2 shadow">
<div className="search"> <div className="flex items-center gap-1">
<img src={searchIcon} alt="search icon" /> <img src={searchIcon} alt="search icon" />
<input value={input} placeholder={`${t("action.search")}...`} className="input" onChange={handleInput} /> <input value={input} placeholder={`${t("action.search")}...`} className="w-full text-sm" onChange={handleInput} />
</div> </div>
<Tooltip tip={t("more")} placement="bottom"> <Tooltip tip={t("more")} placement="bottom">
<Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}> <Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}>
<img src={addIcon} alt="add icon" className="cursor-pointer" /> <img src={addIcon} alt="add icon" className="cursor-pointer" />
</Tippy> </Tippy>
</Tooltip> </Tooltip>
</StyledWrapper> </div>
); );
}; };
export default Search; export default Search;