refactor: add typescript support to project

This commit is contained in:
HD
2022-06-29 16:54:58 +08:00
parent ccfee28e13
commit 003353e43f
33 changed files with 358 additions and 224 deletions
+5 -3
View File
@@ -51,7 +51,7 @@ export const authApi = createApi({
}
}
}),
register: builder.mutation({
register: builder.mutation<any, any>({
query: (data) => ({
url: `user/register`,
method: "POST",
@@ -143,10 +143,12 @@ export const authApi = createApi({
url: `/user/check_email?email=${encodeURIComponent(email)}`
})
}),
getCredentials: builder.query({
// todo: check return type
getCredentials: builder.query<any, void>({
query: () => ({ url: "/token/credentials" })
}),
logout: builder.query({
// todo: check return type
logout: builder.query<any, void>({
query: () => ({ url: "token/logout" }),
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
+5 -4
View File
@@ -10,11 +10,12 @@ import {
LoginConfig,
Server,
TestEmailDTO,
NewAdminDTO,
CreateAdminDTO,
SMTPConfig,
AgoraConfig,
GithubAuthConfig
} from "../../types/server";
const defaultExpireDuration = 7 * 24 * 60 * 60;
export const serverApi = createApi({
@@ -169,7 +170,7 @@ export const serverApi = createApi({
}),
updateServer: builder.mutation<void, Server>({
query: (data) => ({
url: `admin/system/organization`,
url: "admin/system/organization",
method: "POST",
body: data
}),
@@ -184,9 +185,9 @@ export const serverApi = createApi({
}
}
}),
createAdmin: builder.mutation<User, NewAdminDTO>({
createAdmin: builder.mutation<User, CreateAdminDTO>({
query: (data) => ({
url: `/admin/system/create_admin`,
url: "/admin/system/create_admin",
method: "POST",
body: data
})
+2 -2
View File
@@ -64,9 +64,9 @@ const StyledWrapper = styled.div`
interface Props {
url?: string;
name?: string;
type?: "user";
type?: "user" | "channel";
disabled?: boolean;
uploadImage: (file: File) => Promise<any>;
uploadImage: (file: File) => void;
}
const AvatarUploader: FC<Props> = ({
+2 -2
View File
@@ -6,7 +6,7 @@ import LockHashIcon from "../../assets/icons/channel.private.svg";
interface Props {
personal?: boolean;
muted?: boolean;
className: string;
className?: string;
}
const Styled = styled.div`
@@ -16,7 +16,7 @@ const Styled = styled.div`
}
`;
const ChannelIcon: FC<Props> = ({ personal = false, muted = false, className }) => {
const ChannelIcon: FC<Props> = ({ personal = false, muted = false, className = "" }) => {
return (
<Styled className={`${muted ? "muted" : ""} ${className}`}>
{personal ? <LockHashIcon /> : <HashIcon />}
+9 -13
View File
@@ -10,19 +10,13 @@ import StyledCheckbox from "../styled/Checkbox";
import useFilteredUsers from "../../hook/useFilteredUsers";
import { useCreateChannelMutation } from "../../../app/services/channel";
import { useAppSelector } from "../../../app/store";
import { CreateChannelDTO } from "../../../types/channel";
interface Props {
personal?: boolean;
closeModal: () => void;
}
interface CreateChannelDTO {
name: string;
description: string;
members?: number[];
is_public: boolean;
}
const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
const { contactsData, loginUid } = useAppSelector((store) => {
return { contactsData: store.contacts.byId, loginUid: store.authData.uid };
@@ -31,7 +25,7 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
const [data, setData] = useState<CreateChannelDTO>({
name: "",
description: "",
members: [loginUid],
members: loginUid ? [Number(loginUid)] : [],
is_public: !personal
});
@@ -82,13 +76,15 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
};
const toggleCheckMember = ({ currentTarget }: MouseEvent<HTMLLIElement>) => {
const { members } = data;
const members = data.members ?? [];
const { uid } = currentTarget.dataset;
let tmp = members.includes(+uid) ? members.filter((m) => m != uid) : [...members, +uid];
const uidNum = Number(uid);
let tmp = members.includes(uidNum) ? members.filter((m) => m != uidNum) : [...members, uidNum];
setData((prev) => ({ ...prev, members: tmp }));
};
const loginUser = contactsData[loginUid];
if (!loginUid) return null;
const loginUser = contactsData[Number(loginUid)];
if (!loginUser) return null;
const { name, members, is_public } = data;
@@ -108,13 +104,13 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
<ul className="users">
{contacts.map((u) => {
const { uid } = u;
const checked = members.includes(uid);
const checked = members ? members.includes(uid) : false;
return (
<li
key={uid}
data-uid={uid}
className="user"
onClick={loginUid == uid ? null : toggleCheckMember}
onClick={loginUid == uid ? undefined : toggleCheckMember}
>
<StyledCheckbox
disabled={loginUid == uid}
+9 -10
View File
@@ -37,13 +37,12 @@ const GoogleLoginButton: FC<Props> = ({ type = "login", clientId }) => {
console.error("google login script load failure", wtf);
},
clientId,
onSuccess: ({ tokenId, ...rest }) => {
console.info("google oauth success", tokenId, rest);
login({
magic_token,
id_token: tokenId,
type: "google"
});
onSuccess: (res) => {
if ("code" in res) {
console.error(`google login failed: ${res.code}`);
} else {
login({ magic_token, id_token: res.tokenId, type: "google" });
}
},
onFailure: (wtf) => {
console.error("google login failure", wtf);
@@ -57,7 +56,7 @@ const GoogleLoginButton: FC<Props> = ({ type = "login", clientId }) => {
}
}, [isSuccess]);
useEffect(() => {
if (error) {
if (error && "status" in error) {
switch (error.status) {
case 410:
toast.error(
@@ -71,14 +70,14 @@ const GoogleLoginButton: FC<Props> = ({ type = "login", clientId }) => {
return;
}
}, [error]);
const handleGoogleLogin = () => {
signIn();
};
// console.log("google login ", loaded);
return (
<StyledSocialButton disabled={!loaded || isLoading} onClick={handleGoogleLogin}>
<IconGoogle className="icon" alt="google icon" />
<IconGoogle className="icon" />
{loaded ? `${type === "login" ? "Sign in" : "Sign up"} with Google` : `Initializing`}
</StyledSocialButton>
);
+19 -12
View File
@@ -1,18 +1,21 @@
import { FC, ReactElement } from "react";
import { FC, ReactNode } from "react";
import styled from "styled-components";
import { useLocation, NavLink } from "react-router-dom";
import backIcon from "../../assets/icons/arrow.left.svg?url";
import { Nav } from "../../routes/settingChannel/navs";
const StyledWrapper = styled.div`
width: 100vw;
height: 100vh;
display: flex;
> .left {
max-height: 100vh;
overflow: scroll;
padding: 32px 16px;
min-width: 212px;
background-color: #f5f6f7;
> .title {
cursor: pointer;
font-weight: bold;
@@ -21,16 +24,16 @@ const StyledWrapper = styled.div`
color: #1c1c1e;
margin-bottom: 32px;
padding-left: 24px;
background: url(${backIcon});
background-size: 16px;
background-repeat: no-repeat;
background-position: left;
background: url(${backIcon}) no-repeat left;
}
> .items {
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 36px;
&:before {
padding-left: 12px;
content: attr(data-title);
@@ -40,31 +43,37 @@ const StyledWrapper = styled.div`
color: #6b7280;
margin-bottom: 2px;
}
.item {
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #44494f;
border-radius: 4px;
&:hover,
&.curr {
background: #e7e5e4;
}
> a {
display: block;
padding: 4px 12px;
}
}
&.danger .item {
cursor: pointer;
padding: 4px 12px;
color: #ef4444;
&:hover {
background: none;
}
}
}
}
> .right {
background-color: #fff;
width: 100%;
@@ -72,6 +81,7 @@ const StyledWrapper = styled.div`
/* max-height: -webkit-fill-available; */
overflow: auto;
padding: 32px;
> .title {
font-weight: bold;
font-size: 20px;
@@ -82,21 +92,18 @@ const StyledWrapper = styled.div`
}
`;
interface Nav {
name?: string;
export interface Danger {
title: string;
// todo
items: [];
component: ReactElement;
handler: () => void;
}
interface Props {
closeModal: () => void;
title?: string;
navs: Nav[];
dangers: [];
nav: Nav;
children: ReactElement;
dangers: Danger[];
nav: { title: string; name?: string; component?: ReactNode };
children: ReactNode;
}
const StyledSettingContainer: FC<Props> = ({
+23 -10
View File
@@ -1,4 +1,4 @@
import { useState } from "react";
import { FC, useState } from "react";
import styled from "styled-components";
import Tippy from "@tippyjs/react";
import IconSelect from "../../../assets/icons/check.sign.svg";
@@ -26,21 +26,34 @@ const Styled = styled.div`
}
`;
const CURRENT_NOT_SET = {};
export interface Option {
icon?: string;
title: string;
value: string;
selected: boolean;
underline?: boolean;
}
export default function Select({ options = [], updateSelect = null, current = CURRENT_NOT_SET }) {
interface Props {
options: Option[];
updateSelect: null | ((option: Partial<Option>) => void);
current: null | Partial<Option>;
}
const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }) => {
const [optionsVisible, setOptionsVisible] = useState(false);
const [curr, setCurr] = useState(undefined);
const [curr, setCurr] = useState<Partial<Option> | null>(null);
const toggleVisible = () => {
setOptionsVisible((prev) => !prev);
};
const handleSelect = (data) => {
const handleSelect = (data: Partial<Option>) => {
setCurr(data);
toggleVisible();
if (updateSelect) {
updateSelect(data);
}
};
return (
<Tippy
visible={optionsVisible}
@@ -52,7 +65,7 @@ export default function Select({ options = [], updateSelect = null, current = CU
{options.map(({ title, value, selected, underline }) => {
return (
<li
onClick={selected ? null : handleSelect.bind(null, { title, value })}
onClick={selected ? undefined : handleSelect.bind(null, { title, value })}
className={`item sb ${underline ? "underline" : ""}`}
data-disabled={selected}
key={value}
@@ -66,11 +79,11 @@ export default function Select({ options = [], updateSelect = null, current = CU
}
>
<Styled onClick={toggleVisible}>
<span className="txt">
{(current !== CURRENT_NOT_SET ? current : curr)?.title || `Select`}
</span>
<span className="txt">{(current !== null ? current : curr)?.title || "Select"}</span>
<IconArrow className="icon" />
</Styled>
</Tippy>
);
}
};
export default Select;
+4
View File
@@ -11,6 +11,10 @@ import { useLazyLogoutQuery } from "../../app/services/auth";
export default function useLogout() {
const dispatch = useDispatch();
const [logout, { isLoading, isSuccess }] = useLazyLogoutQuery();
// todo: remove batch
// If you're using React 18, you do not need to use the batch API. React 18 automatically
// batches all state updates, no matter where they're queued.
// ref: https://react-redux.js.org/api/batch
const clearLocalData = () => {
batch(() => {
dispatch(resetFootprint());
+2 -2
View File
@@ -1,10 +1,10 @@
import { useState, useEffect } from "react";
import { normalizeArchiveData } from "../utils";
import { normalizeArchiveData, ArchiveMessage } from "../utils";
import { useLazyGetArchiveMessageQuery } from "../../app/services/message";
export default function useNormalizeMessage() {
const [filePath, setFilePath] = useState<string | null>(null);
const [normalizedMessages, setNormalizedMessages] = useState(null);
const [normalizedMessages, setNormalizedMessages] = useState<ArchiveMessage[] | null>(null);
const [getArchiveMessage, { data, isError, isLoading, isSuccess }] =
useLazyGetArchiveMessageQuery();
useEffect(() => {
+7 -4
View File
@@ -1,6 +1,6 @@
import { Suspense, useEffect, lazy } from "react";
import { Route, Routes, HashRouter } from "react-router-dom";
import { Provider, useSelector } from "react-redux";
import { Provider } from "react-redux";
import toast from "react-hot-toast";
import NotFoundPage from "./404";
// import Welcome from './Welcome'
@@ -25,16 +25,19 @@ import Meta from "../common/component/Meta";
import HomePage from "./home";
import ChatPage from "./chat";
import Loading from "../common/component/Loading";
import store from "../app/store";
import store, { useAppSelector } from "../app/store";
const PageRoutes = () => {
const { ui: { online }, fileMessages } = useSelector((store) => {
const {
ui: { online },
fileMessages
} = useAppSelector((store) => {
return { ui: store.ui, fileMessages: store.fileMessage };
});
// 掉线检测
useEffect(() => {
let toastId = '0';
let toastId = "0";
if (!online) {
toast.error("Network Offline!", { duration: Infinity });
} else {
+33 -24
View File
@@ -1,79 +1,84 @@
import { useState, useEffect } from "react";
import { useState, useEffect, FormEvent, ChangeEvent, FC } from "react";
import StyledWrapper from "./styled";
import { Navigate } from "react-router-dom";
import toast from "react-hot-toast";
import BASE_URL from "../../app/config";
import { useRegisterMutation } from "../../app/services/auth";
import { useCheckMagicTokenValidMutation } from "../../app/services/auth";
import { useSelector } from "react-redux";
import { useAppSelector } from "../../app/store";
export default function InvitePage() {
const { token: loginToken } = useSelector((store) => store.authData);
interface AuthForm {
name: string;
email: string;
password: string;
}
const InvitePage: FC = () => {
const { token: loginToken } = useAppSelector((store) => store.authData);
const [secondPwd, setSecondPwd] = useState("");
const [samePwd, setSamePwd] = useState(true);
const [token, setToken] = useState("");
const [token, setToken] = useState<string | null>("");
const [valid, setValid] = useState(false);
// const [sp] = useSearchParams();
// const navigateTo = useNavigate();
const [register, { data, isLoading, isSuccess, isError, error }] = useRegisterMutation();
const [checkToken, { data: isValid, isLoading: checkLoading, isSuccess: checkSuccess }] =
useCheckMagicTokenValidMutation();
useEffect(() => {
// console.log(search);
const query = new URLSearchParams(location.search);
setToken(query.get("token"));
}, []);
useEffect(() => {
if (token) {
checkToken(token);
}
}, [token]);
useEffect(() => {
if (checkSuccess) {
console.log({ isValid });
setValid(isValid);
} else {
setValid(false);
}
}, [checkSuccess, isValid]);
const [input, setInput] = useState({
name: "",
email: "",
password: ""
});
const [input, setInput] = useState<AuthForm>({ name: "", email: "", password: "" });
const handleReg = (evt) => {
const handleReg = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
if (!samePwd) {
toast.error("two passwords not same");
return;
}
console.log("wtf", input);
register({
...input,
magic_token: token,
gender: 1
});
};
const handleInput = (evt) => {
const { type } = evt.target.dataset;
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
const { type } = evt.target.dataset as { type: keyof AuthForm };
const { value } = evt.target;
console.log(type, value);
setInput((prev) => {
prev[type] = value;
return { ...prev };
});
};
const handleSecondPwdInput = (evt) => {
const handleSecondPwdInput = (evt: ChangeEvent<HTMLInputElement>) => {
const { value } = evt.target;
setSecondPwd(value);
};
const handlePwdCheck = () => {
if (secondPwd) {
setSamePwd(secondPwd == input.password);
}
};
useEffect(() => {
if (!samePwd) {
toast.error("two passwords not same");
@@ -88,8 +93,7 @@ export default function InvitePage() {
location.href = `/#/login`;
// navigateTo("/login",);
}, 500);
} else if (isError) {
console.log("register failed", error);
} else if (isError && error && "data" in error) {
switch (error.status) {
case 400:
toast.error("Register Failed: please check inputs");
@@ -102,6 +106,8 @@ export default function InvitePage() {
email_conflict: "email conflict",
name_conflict: "name conflict"
};
// todo
// @ts-ignore
toast.error(`Register Failed: ${tips[error.data?.reason]}`);
break;
}
@@ -114,9 +120,10 @@ export default function InvitePage() {
const { email, password, name } = input;
if (loginToken) return <Navigate replace to="/" />;
if (!token) return "token not found";
if (checkLoading) return "checking token valid";
if (!valid) return "invite token expires or invalid";
if (!token) return <>token not found</>;
if (checkLoading) return <>checking token valid</>;
if (!valid) return <>invite token expires or invalid</>;
return (
<StyledWrapper>
<div className="form animate__animated animate__fadeInDown animate__faster">
@@ -168,4 +175,6 @@ export default function InvitePage() {
</div>
</StyledWrapper>
);
}
};
export default InvitePage;
+9 -4
View File
@@ -1,4 +1,4 @@
import React from "react";
import React, { FC } from "react";
import { Helmet } from "react-helmet";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
@@ -11,9 +11,14 @@ import DonePage from "./steps/donePage";
import useServerSetup, { steps } from "./useServerSetup";
import StyledOnboardingPage from "./styled";
function Navigator({ step, setStep }) {
interface Props {
step: string;
setStep: (step: string) => void;
}
const Navigator: FC<Props> = ({ step, setStep }) => {
const index = steps.map((value) => value.name).indexOf(step);
const canJumpTo = steps.find((value) => value.name === step).canJumpTo || [];
const canJumpTo = steps.find((value) => value.name === step)?.canJumpTo || [];
return (
<div className="navigator">
@@ -41,7 +46,7 @@ function Navigator({ step, setStep }) {
})}
</div>
);
}
};
export default function OnboardingPage() {
const serverSetup = useServerSetup();
+11 -4
View File
@@ -1,7 +1,14 @@
import { useCallback, useState } from "react";
import { Swiper } from "swiper/types";
// `name` for in-code usage, `label` for display
const steps = [
export interface Step {
name: string;
label: string;
canJumpTo?: string[];
}
const steps: Step[] = [
{
name: "welcomePage",
label: "Welcome Page"
@@ -31,12 +38,12 @@ const steps = [
];
export default function useServerSetup() {
const [swiper, setSwiper] = useState(null);
const [swiper, setSwiper] = useState<Swiper | null>(null);
const [index, setIndex] = useState(0);
const [index, setIndex] = useState<number>(0);
const step = steps[index].name;
const setStep = useCallback(
(name) => {
(name: string) => {
const newIndex = steps.map((step) => step.name).indexOf(name);
setIndex(newIndex);
if (swiper !== null) {
-1
View File
@@ -1,4 +1,3 @@
// import React from "react";
import styled from "styled-components";
const Styled = styled.div`
+23 -18
View File
@@ -1,17 +1,15 @@
/* eslint-disable no-undef */
import { useState, useEffect } from "react";
import { useState, useEffect, FC, FormEvent, ChangeEvent } from "react";
import { useDispatch } from "react-redux";
import { useParams } from "react-router-dom";
import toast from "react-hot-toast";
import { setAuthData } from "../../app/slices/auth.data";
import Input from "../../common/component/styled/Input";
import Button from "../../common/component/styled/Button";
import { useLoginMutation, useCheckMagicTokenValidMutation } from "../../app/services/auth";
import toast from "react-hot-toast";
import ExpiredTip from "./ExpiredTip";
import { useRegisterMutation } from "../../app/services/auth";
export default function RegWithUsername() {
const RegWithUsername: FC = () => {
const [checkTokenInvalid, { data: isTokenValid, isLoading: checkingToken }] =
useCheckMagicTokenValidMutation();
const [
@@ -28,7 +26,8 @@ export default function RegWithUsername() {
const [username, setUsername] = useState("");
const query = new URLSearchParams(location.search);
// const githubCode = query.get("gcode");
const token = query.get("magic_token");
// todo: check if query param exists
const token = query.get("magic_token") as string;
useEffect(() => {
if (token) {
checkTokenInvalid(token);
@@ -36,25 +35,29 @@ export default function RegWithUsername() {
}, [token]);
useEffect(() => {
switch (loginError?.status) {
if (loginError && "status" in loginError) {
switch (loginError.status) {
case 401:
toast.error("Invalided Token");
break;
default:
break;
}
}
}, [loginError]);
useEffect(() => {
switch (regError?.status) {
if (regError && "status" in regError) {
switch (regError.status) {
case 409:
toast.error("Something Conflicted!");
break;
default:
break;
}
}
}, [regError]);
useEffect(() => {
const isSuccess = loginSuccess || regSuccess;
const data = loginData || regData;
@@ -67,7 +70,7 @@ export default function RegWithUsername() {
}
}, [loginSuccess, regSuccess, loginData, regData]);
const handleAuth = (evt) => {
const handleAuth = (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
if (from == "reg") {
register({
@@ -83,13 +86,12 @@ export default function RegWithUsername() {
}
};
const handleInput = (evt) => {
const { value } = evt.target;
setUsername(value);
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
setUsername(evt.target.value);
};
console.log("ffff", from);
if (!token) return "No Token";
if (checkingToken) return "Checking Magic Link...";
if (!token) return <>"No Token"</>;
if (checkingToken) return <>"Checking Magic Link..."</>;
if (!isTokenValid) return <ExpiredTip />;
const isLoading = loginLoading || regLoading;
const isSuccess = loginSuccess || regSuccess;
@@ -112,9 +114,12 @@ export default function RegWithUsername() {
onChange={handleInput}
/>
<Button type="submit" disabled={isLoading || !username || isSuccess}>
{/* todo typo */}
{isLoading ? "Logining" : `Continue`}
</Button>
</form>
</>
);
}
};
export default RegWithUsername;
+16 -9
View File
@@ -1,6 +1,5 @@
import { useState, useEffect } from "react";
import { useState, useEffect, ChangeEvent, FormEvent } from "react";
import toast from "react-hot-toast";
// import { useNavigate } from "react-router-dom";
import BASE_URL, { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
import Input from "../../common/component/styled/Input";
import Button from "../../common/component/styled/Button";
@@ -13,13 +12,19 @@ import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
import GoogleLoginButton from "../../common/component/GoogleLoginButton";
import GithubLoginButton from "../../common/component/GithubLoginButton";
interface AuthForm {
email: string;
password: string;
confirmPassword: string;
}
export default function Reg() {
const [sendRegMagicLink, { isLoading: signingUp, data, isSuccess }] =
useSendRegMagicLinkMutation();
const [checkEmail, { isLoading: checkingEmail }] = useLazyCheckEmailQuery();
// const navigateTo = useNavigate();
const [magicToken, setMagicToken] = useState("");
const [input, setInput] = useState({
const [input, setInput] = useState<AuthForm>({
email: "",
password: "",
confirmPassword: ""
@@ -47,7 +52,7 @@ export default function Reg() {
}
}, [isSuccess, data]);
const handleReg = async (evt) => {
const handleReg = async (evt: FormEvent<HTMLFormElement>) => {
evt.preventDefault();
const { email, password, confirmPassword } = input;
if (password !== confirmPassword) {
@@ -55,7 +60,6 @@ export default function Reg() {
return;
}
const { data: canReg } = await checkEmail(email);
console.log("can reg", canReg);
if (canReg) {
sendRegMagicLink({
magic_token: magicToken,
@@ -67,21 +71,23 @@ export default function Reg() {
}
// sendMagicLink(email);
};
const handleCompare = () => {
const { password, confirmPassword } = input;
if (password !== confirmPassword) {
toast.error("Not Same Password!");
}
};
const handleInput = (evt) => {
const { type } = evt.target.dataset;
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
const { type } = evt.target.dataset as { type: keyof AuthForm };
const { value } = evt.target;
// console.log(type, value);
setInput((prev) => {
prev[type] = value;
return { ...prev };
});
};
const { clientId } = useGoogleAuthConfig();
const { config: githubAuthConfig } = useGithubAuthConfig();
const { data: loginConfig, isSuccess: loginConfigSuccess } = useGetLoginConfigQuery();
@@ -94,10 +100,11 @@ export default function Reg() {
const googleLogin = enableGoogleLogin && clientId;
// magic token 没有并且没有开放注册
if (whoCanSignUp !== "EveryOne" && !magicToken)
return `Sign up method is updated to Invitation Link Only`;
return <>Sign up method is updated to Invitation Link Only</>;
const { email, password, confirmPassword } = input;
if (data?.mail_is_sent) return <EmailNextTip />;
const isLoading = signingUp || checkingEmail;
return (
<>
<div className="tips">
-2
View File
@@ -2,8 +2,6 @@ import { Outlet } from "react-router-dom";
import StyledWrapper from "./styled";
export default function RegContainer() {
// const isRegHome = useMatch(`/register`);
return (
<StyledWrapper>
<div className="form">
+12 -3
View File
@@ -1,3 +1,4 @@
import { ChangeEvent, FC } from "react";
import styled from "styled-components";
import iconSearch from "../../assets/icons/search.svg?url";
@@ -24,8 +25,14 @@ const Styled = styled.div`
}
`;
export default function Search({ value = "", updateSearchValue = null, embed = false }) {
const handleChange = (evt) => {
interface Props {
value?: string;
updateSearchValue?: (value: string) => void;
embed?: boolean;
}
const Search: FC<Props> = ({ value = "", updateSearchValue = null, embed = false }) => {
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
if (updateSearchValue) {
updateSearchValue(evt.target.value);
}
@@ -36,4 +43,6 @@ export default function Search({ value = "", updateSearchValue = null, embed = f
<input value={value} onChange={handleChange} className="search" placeholder="Search..." />
</Styled>
);
}
};
export default Search;
+2 -4
View File
@@ -22,8 +22,7 @@ export default function SendMagicLinkPage() {
}, [isSuccess]);
useEffect(() => {
if (error) {
console.log(error);
if (error && "status" in error) {
switch (error.status) {
case "PARSING_ERROR":
toast.error(error.data);
@@ -32,7 +31,7 @@ export default function SendMagicLinkPage() {
toast.error("Username or Password Incorrect");
break;
case 404:
toast.error("Account not exsit");
toast.error("Account not exist");
break;
default:
toast.error("Something Error");
@@ -53,7 +52,6 @@ export default function SendMagicLinkPage() {
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
const { value } = evt.target;
console.log(value);
setEmail(value);
};
+2 -1
View File
@@ -1,4 +1,5 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
display: flex;
justify-content: center;
@@ -8,7 +9,7 @@ const StyledWrapper = styled.div`
max-width: 440px;
padding: 32px 40px 32px 40px;
/* border: 1px solid #eee; */
box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
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;
+2 -2
View File
@@ -86,10 +86,10 @@ export default function APIConfig() {
Are you sure to update API secret? Previous secret will be invalided
</div>
<div className="btns">
<Button onClick={hideAll} className="cancel small">
<Button onClick={() => hideAll()} className="cancel small">
Cancel
</Button>
<Button disabled={isLoading} className="small danger" onClick={updateSecret}>
<Button disabled={isLoading} className="small danger" onClick={() => updateSecret()}>
Yes
</Button>
</div>
+18 -11
View File
@@ -1,12 +1,12 @@
// import React from "react";
import { useEffect, useState } from "react";
import { ChangeEvent, FC, useEffect, useState } from "react";
import styled from "styled-components";
// import { useNavigate } from "react-router-dom";
import toast from "react-hot-toast";
import Modal from "../../common/component/Modal";
import StyledModal from "../../common/component/styled/Modal";
import Button from "../../common/component/styled/Button";
import Checkbox from "../../common/component/styled/Checkbox";
import useLogout from "../../common/hook/useLogout";
const StyledConfirm = styled(StyledModal)`
.clear {
font-weight: normal;
@@ -26,21 +26,25 @@ const StyledConfirm = styled(StyledModal)`
}
}
`;
import Modal from "../../common/component/Modal";
import toast from "react-hot-toast";
export default function LogoutConfirmModal({ closeModal }) {
interface Props {
closeModal: () => void;
}
const LogoutConfirmModal: FC<Props> = ({ closeModal }) => {
const [clearLocal, setClearLocal] = useState(false);
const { logout, exited, exiting, clearLocalData } = useLogout();
const handleLogout = () => {
logout();
};
const handleCheck = (evt) => {
const handleCheck = (evt: ChangeEvent<HTMLInputElement>) => {
setClearLocal(evt.target.checked);
};
useEffect(() => {
if (exited) {
if (clearLocal) {
console.log("clear all store");
clearLocalData();
}
toast.success("Logout Successfully");
@@ -50,6 +54,7 @@ export default function LogoutConfirmModal({ closeModal }) {
// location.reload();
}
}, [exited, clearLocal]);
return (
<Modal id="modal-modal">
<StyledConfirm
@@ -59,7 +64,7 @@ export default function LogoutConfirmModal({ closeModal }) {
<>
<Button onClick={closeModal}>Cancel</Button>
<Button onClick={handleLogout} className="danger">
{exiting ? "Logging out" : `Log Out`}
{exiting ? "Logging out" : "Log Out"}
</Button>
</>
}
@@ -74,4 +79,6 @@ export default function LogoutConfirmModal({ closeModal }) {
</StyledConfirm>
</Modal>
);
}
};
export default LogoutConfirmModal;
+16 -6
View File
@@ -1,5 +1,4 @@
// import React from "react";
import { useEffect, useState } from "react";
import { ChangeEvent, FC, useEffect, useState } from "react";
import styled from "styled-components";
import toast from "react-hot-toast";
import Input from "../../common/component/styled/Input";
@@ -24,18 +23,27 @@ const StyledEdit = styled(StyledModal)`
}
`;
export default function ProfileBasicEditModal({
interface Props {
label?: string;
valueKey?: string;
value?: string;
title?: string;
intro?: string;
closeModal: () => void;
}
const ProfileBasicEditModal: FC<Props> = ({
label = "Username",
valueKey = "name",
value = "",
title = "Change your username",
intro = "Enter a new username and your existing password.",
closeModal
}) {
}) => {
const [input, setInput] = useState(value);
// const dispatch = useDispatch();
const [update, { isLoading, isSuccess }] = useUpdateInfoMutation();
const handleChange = (evt) => {
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
setInput(evt.target.value);
};
const handleUpdate = () => {
@@ -69,4 +77,6 @@ export default function ProfileBasicEditModal({
</StyledEdit>
</Modal>
);
}
};
export default ProfileBasicEditModal;
+8 -2
View File
@@ -27,9 +27,15 @@ interface Props {
closeModal: () => void;
}
interface BaseForm {
current: string;
newPassword: string;
confirmPassword: string;
}
const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
const { data } = useGetCredentialsQuery();
const [input, setInput] = useState({
const [input, setInput] = useState<BaseForm>({
current: "",
newPassword: "",
confirmPassword: ""
@@ -37,7 +43,7 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
// const dispatch = useDispatch();
const [updatePassword, { isLoading, isSuccess }] = useUpdatePasswordMutation();
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
const { type } = evt.target.dataset;
const { type } = evt.target.dataset as { type: keyof BaseForm };
setInput((prev) => {
return { ...prev, [type]: evt.target.value };
});
+22 -8
View File
@@ -1,15 +1,25 @@
import { ChangeEvent, useState } from "react";
import Select from "../../../../common/component/styled/Select";
import { ChangeEvent, FC, useState } from "react";
import Select, { Option } from "../../../../common/component/styled/Select";
import Button from "../../../../common/component/styled/Button";
import Input from "../../../../common/component/styled/Input";
import Toggle from "../../../../common/component/styled/Toggle";
import options from "./items.json";
import Styled from "./styled";
// import IconPlus from "../../../../assets/icons/plus.circle.svg";
import IconMinus from "../../../../assets/icons/minus.circle.svg";
export default function IssuerList({ issuers = [], onChange }) {
const [select, setSelect] = useState({});
interface Issuer {
domain: string;
enable: boolean;
favicon: string;
}
interface Props {
issuers: Issuer[];
onChange: (issuers: Issuer[]) => void;
}
const IssuerList: FC<Props> = ({ issuers = [], onChange }) => {
const [select, setSelect] = useState<Partial<Option> | null>(null);
const [newDomain, setNewDomain] = useState("");
const handleNewDomain = (evt: ChangeEvent<HTMLInputElement>) => {
@@ -87,7 +97,9 @@ export default function IssuerList({ issuers = [], onChange }) {
<Button
disabled={disableBtn}
onClick={() => {
const { icon, value } = options.find((option) => option.value === select.value);
const found = options.find((option) => option.value === select?.value);
if (!found) return;
const { icon, value } = found;
onChange(
issuers.concat({
enable: true,
@@ -95,7 +107,7 @@ export default function IssuerList({ issuers = [], onChange }) {
domain: value || newDomain
})
);
setSelect({});
setSelect(null);
setNewDomain("");
}}
>
@@ -107,4 +119,6 @@ export default function IssuerList({ issuers = [], onChange }) {
{/* <IconPlus className="add" /> */}
</Styled>
);
}
};
export default IssuerList;
+3 -2
View File
@@ -1,8 +1,9 @@
import Tippy from "@tippyjs/react";
import { roundArrow } from "tippy.js";
import "tippy.js/dist/svg-arrow.css";
// import React from 'react'
import styled from "styled-components";
import IconQuestion from "../../../assets/icons/question.svg";
const StyledContent = styled.div`
padding: 8px 12px;
background: #101828;
@@ -15,7 +16,7 @@ const StyledContent = styled.div`
color: #55c7ec;
}
`;
import IconQuestion from "../../../assets/icons/question.svg";
export default function Tooltip({ link = "#" }) {
return (
<Tippy
+10 -11
View File
@@ -9,24 +9,23 @@ let from: string | null = null;
export default function Setting() {
const [searchParams] = useSearchParams();
const navs = useNavs();
const flatenNavs = navs
.map(({ items }) => {
return items;
})
.flat();
const flattenNaves = navs.map(({ items }) => items).flat();
const navKey = searchParams.get("nav");
from = from ?? (searchParams.get("f") || "/");
const [logoutConfirm, setLogoutConfirm] = useState(false);
const navgateTo = useNavigate();
const navigateTo = useNavigate();
const close = () => {
// dispatch(toggleSetting());
navgateTo(from);
// todo: check usage
navigateTo(from!);
from = null;
};
const toggleLogoutConfrim = () => {
const toggleLogoutConfirm = () => {
setLogoutConfirm((prev) => !prev);
};
const currNav = flatenNavs.find((n) => n.name == navKey) || flatenNavs[0];
const currNav = flattenNaves.find((n) => n.name == navKey) || flattenNaves[0];
return (
<>
@@ -35,11 +34,11 @@ export default function Setting() {
closeModal={close}
title="Settings"
navs={navs}
dangers={[{ title: "Log Out", handler: toggleLogoutConfrim }]}
dangers={[{ title: "Log Out", handler: toggleLogoutConfirm }]}
>
{currNav.component}
</StyledSettingContainer>
{logoutConfirm && <LogoutConfirmModal closeModal={toggleLogoutConfrim} />}
{logoutConfirm && <LogoutConfirmModal closeModal={toggleLogoutConfirm} />}
</>
);
}
+15 -7
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, ChangeEvent } from "react";
import styled from "styled-components";
import toast from "react-hot-toast";
import {
@@ -43,37 +43,44 @@ const StyledWrapper = styled.div`
}
}
`;
interface ChannelFormData {
name: string;
description: string;
}
export default function Overview({ id = 0 }) {
const { loginUser, channel } = useAppSelector((store) => {
const { uid } = store.authData;
return {
loginUser: store.contacts.byId[store.authData.uid],
loginUser: uid ? store.contacts.byId[Number(uid)] : undefined,
channel: store.channels.byId[id]
};
});
const { data, refetch } = useGetChannelQuery(id);
const [changed, setChanged] = useState(false);
const [values, setValues] = useState(null);
const [values, setValues] = useState<ChannelFormData>({ name: "", description: "" });
const [updateChannelIcon] = useUpdateIconMutation();
const [updateChannel, { isSuccess: updated }] = useUpdateChannelMutation();
const handleUpdate = () => {
const { name, description } = values;
updateChannel({ id, name, description });
};
const handleChange = (evt) => {
const handleChange = (evt: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const newValue = evt.target.value;
const { type } = evt.target.dataset;
const { type } = evt.target.dataset as { type: keyof ChannelFormData };
setValues((prev) => {
return { ...prev, [type]: newValue };
});
};
const updateIcon = (image) => {
const updateIcon = (image: File) => {
updateChannelIcon({ gid: id, image });
};
const handleReset = () => {
console.log("reset", data);
setValues(data);
};
@@ -100,6 +107,7 @@ export default function Overview({ id = 0 }) {
toast.success("Channel updated!");
refetch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updated]);
if (!values || !id) return null;
+32 -27
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
import LeaveChannel from "../../common/component/LeaveChannel";
import StyledSettingContainer from "../../common/component/StyledSettingContainer";
import StyledSettingContainer, { Danger } from "../../common/component/StyledSettingContainer";
import DeleteConfirmModal from "./DeleteConfirmModal";
import useNavs from "./navs";
import { useAppSelector } from "../../app/store";
@@ -9,40 +9,54 @@ import { useAppSelector } from "../../app/store";
let from: string | null = null;
export default function ChannelSetting() {
const { cid } = useParams();
const { cid } = useParams<{ cid: string }>();
const cidNum = Number(cid);
const { isAdmin, loginUid, channel } = useAppSelector((store) => {
return {
loginUid: store.authData.uid,
isAdmin: store.contacts.byId[store.authData.uid]?.is_admin,
channel: store.channels.byId[cid]
isAdmin: store.authData.uid
? store.contacts.byId[Number(store.authData.uid)]?.is_admin
: false,
channel: store.channels.byId[cidNum]
};
});
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const navs = useNavs(cid);
const flatenNavs = navs
.map(({ items }) => {
return items;
})
.flat();
const navs = useNavs(cidNum);
const flattenNaves = navs.map(({ items }) => items).flat();
const navKey = searchParams.get("nav");
from = from ?? (searchParams.get("f") || "/");
const [deleteConfirm, setDeleteConfirm] = useState(false);
const [leaveConfirm, setLeaveConfirm] = useState(false);
const close = () => {
navigate(from);
// todo: check usage
navigate(from!);
from = null;
};
const toggleDeleteConfrim = () => {
const toggleDeleteConfirm = () => {
setDeleteConfirm((prev) => !prev);
};
const toggleLeaveConfrim = () => {
const toggleLeaveConfirm = () => {
setLeaveConfirm((prev) => !prev);
};
if (!cid) return null;
const currNav = flatenNavs.find((n) => n.name == navKey) || flatenNavs[0];
const canDelete = isAdmin || channel?.owner == loginUid;
const currNav = flattenNaves.find((n) => n.name == navKey) || flattenNaves[0];
const canDelete = isAdmin || channel?.owner === Number(loginUid);
const canLeave = !channel?.is_public;
const dangers: Danger[] = [];
if (canLeave) {
dangers.push({
title: "Leave Channel",
handler: toggleLeaveConfirm
});
}
if (canDelete) {
dangers.push({
title: "Delete Channel",
handler: toggleDeleteConfirm
});
}
return (
<>
@@ -51,21 +65,12 @@ export default function ChannelSetting() {
closeModal={close}
title="Channel Setting"
navs={navs}
dangers={[
canLeave && {
title: "Leave Channel",
handler: toggleLeaveConfrim
},
canDelete && {
title: "Delete Channel",
handler: toggleDeleteConfrim
}
]}
dangers={dangers}
>
{currNav.component}
</StyledSettingContainer>
{deleteConfirm && <DeleteConfirmModal closeModal={toggleDeleteConfrim} id={cid} />}
{leaveConfirm && <LeaveChannel closeModal={toggleLeaveConfrim} id={cid} />}
{deleteConfirm && <DeleteConfirmModal closeModal={toggleDeleteConfirm} id={Number(cid)} />}
{leaveConfirm && <LeaveChannel closeModal={toggleLeaveConfirm} id={cidNum} />}
</>
);
}
+16 -3
View File
@@ -1,7 +1,20 @@
import Overview from "./Overview";
import ManageMembers from "../../common/component/ManageMembers";
import { ReactNode } from "react";
const useNavs = (channelId: number) => {
export interface NavItem {
name: string;
title: string;
component: ReactNode;
}
export interface Nav {
name?: string;
title: string;
items: NavItem[];
}
const useNavs = (cid: number): Nav[] => {
return [
{
title: "General",
@@ -9,12 +22,12 @@ const useNavs = (channelId: number) => {
{
name: "overview",
title: "Overview",
component: <Overview id={channelId} />
component: <Overview id={cid} />
},
{
name: "members",
title: "Members",
component: <ManageMembers cid={channelId} />
component: <ManageMembers cid={cid} />
}
// {
// name: "permissions",
+7
View File
@@ -27,6 +27,13 @@ export interface Channel {
pinned_messages: PinnedMessage[];
}
export interface CreateChannelDTO {
name: string;
description: string;
members?: number[];
is_public: boolean;
}
export interface UpdateChannelDTO {
operation: "add_member" | "remove_member";
members?: number[];
+3 -2
View File
@@ -1,11 +1,12 @@
import { User } from "./auth";
export interface Server {
name: string;
description: string;
}
export interface GithubAuthConfig {
client_id: string;
client_secret: string;
client_secret: string; // todo: check security problem!
}
export interface FirebaseConfig {
enabled: boolean;
@@ -56,6 +57,6 @@ export interface LoginConfig {
metamask: boolean;
third_party: boolean;
}
export interface NewAdminDTO extends Pick<User, "email" | "name" | "gender"> {
export interface CreateAdminDTO extends Pick<User, "email" | "name" | "gender"> {
password: string;
}