diff --git a/.gitignore b/.gitignore index 4d29575d..6c9142c6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ .env.development.local .env.test.local .env.production.local +/.idea npm-debug.log* yarn-debug.log* diff --git a/src/app/services/auth.js b/src/app/services/auth.js index a4b1ad01..49d3e23c 100644 --- a/src/app/services/auth.js +++ b/src/app/services/auth.js @@ -1,165 +1,160 @@ import { createApi } from "@reduxjs/toolkit/query/react"; import { nanoid } from "@reduxjs/toolkit"; import baseQuery from "./base.query"; -import { - setAuthData, - updateToken, - resetAuthData, - updateInitilize, -} from "../slices/auth.data"; +import { setAuthData, updateToken, resetAuthData, updateInitialized } from "../slices/auth.data"; import BASE_URL, { KEY_DEVICE_KEY } from "../config"; const getDeviceId = () => { - let d = localStorage.getItem(KEY_DEVICE_KEY); - if (!d) { - d = `web:${nanoid()}`; - localStorage.setItem(KEY_DEVICE_KEY, d); - } - return d; + let d = localStorage.getItem(KEY_DEVICE_KEY); + if (!d) { + d = `web:${nanoid()}`; + localStorage.setItem(KEY_DEVICE_KEY, d); + } + return d; }; export const authApi = createApi({ - reducerPath: "authApi", - baseQuery, - endpoints: (builder) => ({ - login: builder.mutation({ - query: (credentials) => ({ - url: "token/login", - method: "POST", - body: { - credential: credentials, - device: getDeviceId(), - device_token: "test", - }, - }), - transformResponse: (data) => { - const { avatar_updated_at } = data.user; - data.user.avatar = - avatar_updated_at == 0 - ? "" - : `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}`; - return data; - }, - async onQueryStarted(params, { dispatch, queryFulfilled }) { - try { - const { data } = await queryFulfilled; - if (data) { - console.log("login resp", data); - dispatch(setAuthData(data)); - } - } catch { - console.log("login error"); - } - }, - }), - // 更新token - renew: builder.mutation({ - query: ({ token, refreshToken }) => ({ - url: "/token/renew", - method: "POST", - body: { - token, - refresh_token: refreshToken, - }, - }), - async onQueryStarted(params, { dispatch, queryFulfilled }) { - try { - const { data } = await queryFulfilled; - dispatch(updateToken(data)); - } catch { - dispatch(resetAuthData()); - console.log("renew token error"); - } - }, - }), - // 更新 device token - updateDeviceToken: builder.mutation({ - query: (device_token) => ({ - url: "/token/device_token", - method: "PUT", - body: { - device_token, - }, - }), - }), - // 获取openid - getOpenid: builder.mutation({ - query: ({ issuer, redirect_uri }) => ({ - url: "/token/openid/authorize", - method: "POST", - body: { - issuer, - redirect_uri, - }, - }), - }), - - checkInviteTokenValid: builder.mutation({ - query: (token) => ({ - url: "user/check_invite_magic_token", - method: "POST", - body: { magic_token: token }, - }), - }), - updatePassword: builder.mutation({ - query: ({ old_password, new_password }) => ({ - url: "user/change_password", - method: "POST", - body: { old_password, new_password }, - }), - }), - sendMagicLink: builder.mutation({ - query: (email) => ({ - url: "token/send_magic_link", - method: "POST", - body: { email }, - }), - }), - getMetamaskNonce: builder.query({ - query: (address) => ({ - url: `/token/metamask/nonce?public_address=${address}`, - }), - }), - getCredentials: builder.query({ - query: () => ({ - url: `/token/credentials`, - }), - }), - logout: builder.query({ - query: () => ({ url: `token/logout` }), - async onQueryStarted(params, { dispatch, queryFulfilled }) { - try { - await queryFulfilled; - dispatch(resetAuthData()); - } catch { - console.log("logout error"); - } - }, - }), - getInitialized: builder.query({ - query: () => ({ - url: `/admin/system/initialized`, - }), - async onQueryStarted(params, { dispatch, queryFulfilled }) { - try { - const { data: isInitized } = await queryFulfilled; - dispatch(updateInitilize(isInitized)); - } catch { - console.log("api initialized error"); - } - }, - }), + reducerPath: "authApi", + baseQuery, + endpoints: (builder) => ({ + login: builder.mutation({ + query: (credentials) => ({ + url: "token/login", + method: "POST", + body: { + credential: credentials, + device: getDeviceId(), + device_token: "test" + } + }), + transformResponse: (data) => { + const { avatar_updated_at } = data.user; + data.user.avatar = + avatar_updated_at == 0 + ? "" + : `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}`; + return data; + }, + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + const { data } = await queryFulfilled; + if (data) { + console.log("login resp", data); + dispatch(setAuthData(data)); + } + } catch { + console.log("login error"); + } + } }), + // 更新token + renew: builder.mutation({ + query: ({ token, refreshToken }) => ({ + url: "/token/renew", + method: "POST", + body: { + token, + refresh_token: refreshToken + } + }), + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + const { data } = await queryFulfilled; + dispatch(updateToken(data)); + } catch { + dispatch(resetAuthData()); + console.log("renew token error"); + } + } + }), + // 更新 device token + updateDeviceToken: builder.mutation({ + query: (device_token) => ({ + url: "/token/device_token", + method: "PUT", + body: { + device_token + } + }) + }), + // 获取openid + getOpenid: builder.mutation({ + query: ({ issuer, redirect_uri }) => ({ + url: "/token/openid/authorize", + method: "POST", + body: { + issuer, + redirect_uri + } + }) + }), + + checkInviteTokenValid: builder.mutation({ + query: (token) => ({ + url: "user/check_invite_magic_token", + method: "POST", + body: { magic_token: token } + }) + }), + updatePassword: builder.mutation({ + query: ({ old_password, new_password }) => ({ + url: "user/change_password", + method: "POST", + body: { old_password, new_password } + }) + }), + sendMagicLink: builder.mutation({ + query: (email) => ({ + url: "token/send_magic_link", + method: "POST", + body: { email } + }) + }), + getMetamaskNonce: builder.query({ + query: (address) => ({ + url: `/token/metamask/nonce?public_address=${address}` + }) + }), + getCredentials: builder.query({ + query: () => ({ + url: `/token/credentials` + }) + }), + logout: builder.query({ + query: () => ({ url: `token/logout` }), + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + await queryFulfilled; + dispatch(resetAuthData()); + } catch { + console.log("logout error"); + } + } + }), + getInitialized: builder.query({ + query: () => ({ + url: `/admin/system/initialized` + }), + async onQueryStarted(params, { dispatch, queryFulfilled }) { + try { + const { data: isInitialized } = await queryFulfilled; + dispatch(updateInitialized(isInitialized)); + } catch { + console.log("api initialized error"); + } + } + }) + }) }); export const { - useGetInitializedQuery, - useSendMagicLinkMutation, - useGetCredentialsQuery, - useUpdateDeviceTokenMutation, - useGetOpenidMutation, - useRenewMutation, - useLazyGetMetamaskNonceQuery, - useLoginMutation, - useLazyLogoutQuery, - useCheckInviteTokenValidMutation, - useUpdatePasswordMutation, + useGetInitializedQuery, + useSendMagicLinkMutation, + useGetCredentialsQuery, + useUpdateDeviceTokenMutation, + useGetOpenidMutation, + useRenewMutation, + useLazyGetMetamaskNonceQuery, + useLoginMutation, + useLazyLogoutQuery, + useCheckInviteTokenValidMutation, + useUpdatePasswordMutation } = authApi; diff --git a/src/app/services/base.query.js b/src/app/services/base.query.js index 12a63ce1..38afd257 100644 --- a/src/app/services/base.query.js +++ b/src/app/services/base.query.js @@ -18,6 +18,7 @@ const whiteList = [ "getMetamaskNonce", "renew", "getInitialized", + "createAdmin", ]; const baseQuery = fetchBaseQuery({ baseUrl: BASE_URL, diff --git a/src/app/services/server.js b/src/app/services/server.js index 21925753..7ab67a55 100644 --- a/src/app/services/server.js +++ b/src/app/services/server.js @@ -4,204 +4,218 @@ import { updateInfo } from "../slices/server"; import baseQuery from "./base.query"; const defaultExpireDuration = 7 * 24 * 60 * 60; export const serverApi = createApi({ - reducerPath: "serverApi", - baseQuery, - endpoints: (builder) => ({ - getServer: builder.query({ - query: () => ({ url: `admin/system/organization` }), - transformResponse: (data) => { - data.logo = `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`; - return data; - }, - async onQueryStarted(data, { dispatch, queryFulfilled }) { - try { - const { data: server } = await queryFulfilled; - dispatch(updateInfo(server)); - } catch { - console.log("get server info error"); - } - }, - }), - getThirdPartySecret: builder.query({ - query: () => ({ - // headers: { - // "content-type": "text/plain", - // accept: "text/plain", - // }, - url: `/admin/system/third_party_secret`, - responseHandler: (response) => response.text(), - }), - keepUnusedDataFor: 0, - }), - updateThirdPartySecret: builder.mutation({ - query: () => ({ - url: `/admin/system/third_party_secret`, - method: "POST", - responseHandler: (response) => response.text(), - }), - }), - getMetrics: builder.query({ - query: () => ({ url: `/admin/system/metrics` }), - }), - getServerVersion: builder.query({ - query: () => ({ - headers: { - // "content-type": "text/plain", - accept: "text/plain", - }, - url: `/admin/system/version`, - responseHandler: (response) => response.text(), - }), - }), - getFirebaseConfig: builder.query({ - query: () => ({ url: `admin/fcm/config` }), - }), - getGoogleAuthConfig: builder.query({ - query: () => ({ url: `admin/google_auth/config` }), - }), - updateGoogleAuthConfig: builder.mutation({ - query: (data) => ({ - url: `admin/google_auth/config`, - method: "POST", - body: data, - }), - }), - getGithubAuthConfig: builder.query({ - query: () => ({ url: `admin/github_auth/config` }), - }), - updateGithubAuthConfig: builder.mutation({ - query: (data) => ({ - url: `admin/github_auth/config`, - method: "POST", - body: data, - }), - }), - sendTestEmail: builder.mutation({ - query: (data) => ({ - url: `/admin/system/send_mail`, - method: "POST", - body: data, - }), - }), - updateFirebaseConfig: builder.mutation({ - query: (data) => ({ - url: `admin/fcm/config`, - method: "POST", - body: data, - }), - }), - getAgoraConfig: builder.query({ - query: () => ({ url: `admin/agora/config` }), - }), - updateAgoraConfig: builder.mutation({ - query: (data) => ({ - url: `admin/agora/config`, - method: "POST", - body: data, - }), - }), - getSMTPConfig: builder.query({ - query: () => ({ url: `admin/smtp/config` }), - }), - getSMTPStatus: builder.query({ - query: () => ({ url: `/admin/smtp/enabled` }), - }), - updateSMTPConfig: builder.mutation({ - query: (data) => ({ - url: `admin/smtp/config`, - method: "POST", - body: data, - }), - }), - getLoginConfig: builder.query({ - query: () => ({ url: `admin/login/config` }), - }), - updateLoginConfig: builder.mutation({ - query: (data) => ({ - url: `admin/login/config`, - method: "POST", - body: data, - }), - }), - updateLogo: builder.mutation({ - query: (data) => ({ - headers: { - "content-type": "image/png", - }, - url: `admin/system/organization/logo`, - method: "POST", - body: data, - }), - async onQueryStarted(data, { dispatch, queryFulfilled }) { - try { - await queryFulfilled; - dispatch( - updateInfo({ - logo: `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`, - }) - ); - } catch { - console.log("update server logo error"); - } - }, - }), - createInviteLink: builder.query({ - query: (expired_in = defaultExpireDuration) => ({ - headers: { - "content-type": "text/plain", - accept: "text/plain", - }, - url: `/admin/system/create_invite_link?expired_in=${expired_in}`, - responseHandler: (response) => response.text(), - }), - transformResponse: (link) => { - // 替换掉域名 - const invite = new URL(link); - return `${location.origin}${invite.pathname}${invite.search}${invite.hash}`; - }, - }), - updateServer: builder.mutation({ - query: (data) => ({ - url: `admin/system/organization`, - method: "POST", - body: data, - }), - async onQueryStarted(data, { dispatch, queryFulfilled, getState }) { - const { name: prevName, description: prevDesc } = getState().server; - dispatch(updateInfo(data)); - try { - await queryFulfilled; - } catch { - dispatch(updateInfo({ name: prevName, description: prevDesc })); - } - }, - }), + reducerPath: "serverApi", + baseQuery, + endpoints: (builder) => ({ + getServer: builder.query({ + query: () => ({ url: `admin/system/organization` }), + transformResponse: (data) => { + data.logo = `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}`; + return data; + }, + async onQueryStarted(data, { dispatch, queryFulfilled }) { + try { + const { data: server } = await queryFulfilled; + dispatch(updateInfo(server)); + } catch { + console.log("get server info error"); + } + } }), + getThirdPartySecret: builder.query({ + query: () => ({ + // headers: { + // "content-type": "text/plain", + // accept: "text/plain", + // }, + url: `/admin/system/third_party_secret`, + responseHandler: (response) => response.text() + }), + keepUnusedDataFor: 0 + }), + updateThirdPartySecret: builder.mutation({ + query: () => ({ + url: `/admin/system/third_party_secret`, + method: "POST", + responseHandler: (response) => response.text() + }) + }), + getMetrics: builder.query({ + query: () => ({ url: `/admin/system/metrics` }) + }), + getServerVersion: builder.query({ + query: () => ({ + headers: { + // "content-type": "text/plain", + accept: "text/plain" + }, + url: `/admin/system/version`, + responseHandler: (response) => response.text() + }) + }), + getFirebaseConfig: builder.query({ + query: () => ({ url: `admin/fcm/config` }) + }), + getGoogleAuthConfig: builder.query({ + query: () => ({ url: `admin/google_auth/config` }) + }), + updateGoogleAuthConfig: builder.mutation({ + query: (data) => ({ + url: `admin/google_auth/config`, + method: "POST", + body: data + }) + }), + getGithubAuthConfig: builder.query({ + query: () => ({ url: `admin/github_auth/config` }) + }), + updateGithubAuthConfig: builder.mutation({ + query: (data) => ({ + url: `admin/github_auth/config`, + method: "POST", + body: data + }) + }), + sendTestEmail: builder.mutation({ + query: (data) => ({ + url: `/admin/system/send_mail`, + method: "POST", + body: data + }) + }), + updateFirebaseConfig: builder.mutation({ + query: (data) => ({ + url: `admin/fcm/config`, + method: "POST", + body: data + }) + }), + getAgoraConfig: builder.query({ + query: () => ({ url: `admin/agora/config` }) + }), + updateAgoraConfig: builder.mutation({ + query: (data) => ({ + url: `admin/agora/config`, + method: "POST", + body: data + }) + }), + getSMTPConfig: builder.query({ + query: () => ({ url: `admin/smtp/config` }) + }), + getSMTPStatus: builder.query({ + query: () => ({ url: `/admin/smtp/enabled` }) + }), + updateSMTPConfig: builder.mutation({ + query: (data) => ({ + url: `admin/smtp/config`, + method: "POST", + body: data + }) + }), + getLoginConfig: builder.query({ + query: () => ({ url: `admin/login/config` }) + }), + updateLoginConfig: builder.mutation({ + query: (data) => ({ + url: `admin/login/config`, + method: "POST", + body: data + }) + }), + updateLogo: builder.mutation({ + query: (data) => ({ + headers: { + "content-type": "image/png" + }, + url: `admin/system/organization/logo`, + method: "POST", + body: data + }), + async onQueryStarted(data, { dispatch, queryFulfilled }) { + try { + await queryFulfilled; + dispatch( + updateInfo({ + logo: `${BASE_URL}/resource/organization/logo?t=${new Date().getTime()}` + }) + ); + } catch { + console.log("update server logo error"); + } + } + }), + createInviteLink: builder.query({ + query: (expired_in = defaultExpireDuration) => ({ + headers: { + "content-type": "text/plain", + accept: "text/plain" + }, + url: `/admin/system/create_invite_link?expired_in=${expired_in}`, + responseHandler: (response) => response.text() + }), + transformResponse: (link) => { + // 替换掉域名 + const invite = new URL(link); + return `${location.origin}${invite.pathname}${invite.search}${invite.hash}`; + } + }), + updateServer: builder.mutation({ + query: (data) => ({ + url: `admin/system/organization`, + method: "POST", + body: data + }), + async onQueryStarted(data, { dispatch, queryFulfilled, getState }) { + const { name: prevName, description: prevDesc } = getState().server; + dispatch(updateInfo(data)); + try { + await queryFulfilled; + } catch { + dispatch(updateInfo({ name: prevName, description: prevDesc })); + } + } + }), + createAdmin: builder.mutation({ + query: (data) => ({ + url: `/admin/system/create_admin`, + method: "POST", + body: data + }) + }), + getInitialized: builder.query({ + query: () => ({ + url: `/admin/system/initialized` + }) + }) + }) }); export const { - useGetServerVersionQuery, - useGetGithubAuthConfigQuery, - useUpdateGithubAuthConfigMutation, - useGetGoogleAuthConfigQuery, - useUpdateGoogleAuthConfigMutation, - useGetSMTPStatusQuery, - useSendTestEmailMutation, - useUpdateFirebaseConfigMutation, - useGetFirebaseConfigQuery, - useGetLoginConfigQuery, - useUpdateLoginConfigMutation, - useGetSMTPConfigQuery, - useUpdateSMTPConfigMutation, - useGetAgoraConfigQuery, - useUpdateAgoraConfigMutation, - useGetServerQuery, - useGetMetricsQuery, - useLazyGetServerQuery, - useUpdateServerMutation, - useUpdateLogoMutation, - useCreateInviteLinkQuery, - useLazyCreateInviteLinkQuery, - useGetThirdPartySecretQuery, - useUpdateThirdPartySecretMutation, + useGetServerVersionQuery, + useGetGithubAuthConfigQuery, + useUpdateGithubAuthConfigMutation, + useGetGoogleAuthConfigQuery, + useUpdateGoogleAuthConfigMutation, + useGetSMTPStatusQuery, + useSendTestEmailMutation, + useUpdateFirebaseConfigMutation, + useGetFirebaseConfigQuery, + useGetLoginConfigQuery, + useUpdateLoginConfigMutation, + useGetSMTPConfigQuery, + useUpdateSMTPConfigMutation, + useGetAgoraConfigQuery, + useUpdateAgoraConfigMutation, + useGetServerQuery, + useGetMetricsQuery, + useLazyGetServerQuery, + useUpdateServerMutation, + useUpdateLogoMutation, + useCreateInviteLinkQuery, + useLazyCreateInviteLinkQuery, + useGetThirdPartySecretQuery, + useUpdateThirdPartySecretMutation, + useCreateAdminMutation, + useGetInitializedQuery } = serverApi; diff --git a/src/app/slices/auth.data.js b/src/app/slices/auth.data.js index 9de9da1f..cafd2a22 100644 --- a/src/app/slices/auth.data.js +++ b/src/app/slices/auth.data.js @@ -1,89 +1,78 @@ import { createSlice } from "@reduxjs/toolkit"; -import { - KEY_PWA_INSTALLED, - KEY_REFRESH_TOKEN, - KEY_TOKEN, - KEY_UID, - KEY_EXPIRE, -} from "../config"; +import { KEY_PWA_INSTALLED, KEY_REFRESH_TOKEN, KEY_TOKEN, KEY_UID, KEY_EXPIRE } from "../config"; const initialState = { - initialized: true, - uid: null, - token: localStorage.getItem(KEY_TOKEN), - expireTime: localStorage.getItem(KEY_EXPIRE) || new Date().getTime(), - refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN), + initialized: true, + uid: null, + token: localStorage.getItem(KEY_TOKEN), + expireTime: localStorage.getItem(KEY_EXPIRE) || new Date().getTime(), + refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN) }; const emptyState = { - initialized: true, - uid: null, - token: null, - expireTime: new Date().getTime(), - refreshToken: null, + initialized: true, + uid: null, + token: null, + expireTime: new Date().getTime(), + refreshToken: null }; const authDataSlice = createSlice({ - name: "authData", - initialState, - reducers: { - setAuthData(state, action) { - const { - initialized = true, - user: { uid }, - token, - refresh_token, - expired_in = 0, - } = action.payload; - state.initialized = initialized; - state.uid = uid; - state.token = token; - state.refreshToken = refresh_token; - // 当前时间往后推expire时长 - console.log("expire", expired_in); - const expireTime = new Date().getTime() + Number(expired_in) * 1000; - state.expireTime = expireTime; - // set local data - localStorage.setItem(KEY_EXPIRE, expireTime); - localStorage.setItem(KEY_TOKEN, token); - localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); - localStorage.setItem(KEY_UID, uid); - }, - resetAuthData() { - console.log("clear auth data"); - // remove local data - localStorage.removeItem(KEY_EXPIRE); - localStorage.removeItem(KEY_TOKEN); - localStorage.removeItem(KEY_REFRESH_TOKEN); - localStorage.removeItem(KEY_UID); - localStorage.removeItem(KEY_PWA_INSTALLED); - - return emptyState; - }, - setUid(state, action) { - const uid = action.payload; - state.uid = uid; - console.log("set uid orginal"); - }, - updateInitilize(state, action) { - const isInitized = action.payload; - state.initialized = isInitized; - }, - updateToken(state, action) { - const { token, refresh_token, expired_in } = action.payload; - console.log("refresh token"); - state.token = token; - const et = new Date().getTime() + Number(expired_in) * 1000; - state.expireTime = et; - state.refreshToken = refresh_token; - localStorage.setItem(KEY_EXPIRE, et); - localStorage.setItem(KEY_TOKEN, token); - localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); - }, + name: "authData", + initialState, + reducers: { + setAuthData(state, action) { + const { + initialized = true, + user: { uid }, + token, + refresh_token, + expired_in = 0 + } = action.payload; + state.initialized = initialized; + state.uid = uid; + state.token = token; + state.refreshToken = refresh_token; + // 当前时间往后推expire时长 + console.log("expire", expired_in); + const expireTime = new Date().getTime() + Number(expired_in) * 1000; + state.expireTime = expireTime; + // set local data + localStorage.setItem(KEY_EXPIRE, expireTime); + localStorage.setItem(KEY_TOKEN, token); + localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); + localStorage.setItem(KEY_UID, uid); }, + resetAuthData() { + console.log("clear auth data"); + // remove local data + localStorage.removeItem(KEY_EXPIRE); + localStorage.removeItem(KEY_TOKEN); + localStorage.removeItem(KEY_REFRESH_TOKEN); + localStorage.removeItem(KEY_UID); + localStorage.removeItem(KEY_PWA_INSTALLED); + + return emptyState; + }, + setUid(state, action) { + const uid = action.payload; + state.uid = uid; + console.log("set uid orginal"); + }, + updateInitialized(state, action) { + const isInitialized = action.payload; + state.initialized = isInitialized; + }, + updateToken(state, action) { + const { token, refresh_token, expired_in } = action.payload; + console.log("refresh token"); + state.token = token; + const et = new Date().getTime() + Number(expired_in) * 1000; + state.expireTime = et; + state.refreshToken = refresh_token; + localStorage.setItem(KEY_EXPIRE, et); + localStorage.setItem(KEY_TOKEN, token); + localStorage.setItem(KEY_REFRESH_TOKEN, refresh_token); + } + } }); -export const { - updateInitilize, - setAuthData, - resetAuthData, - setUid, - updateToken, -} = authDataSlice.actions; +export const { updateInitialized, setAuthData, resetAuthData, setUid, updateToken } = + authDataSlice.actions; export default authDataSlice.reducer; diff --git a/src/assets/icons/play.svg b/src/assets/icons/play.svg new file mode 100644 index 00000000..0bd11821 --- /dev/null +++ b/src/assets/icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/component/styled/Radio.js b/src/common/component/styled/Radio.js new file mode 100644 index 00000000..ef03f715 --- /dev/null +++ b/src/common/component/styled/Radio.js @@ -0,0 +1,91 @@ +import { useRef, useState } from "react"; +import styled from "styled-components"; +import { nanoid } from "@reduxjs/toolkit"; + +const StyledForm = styled.form` + > .option { + &:not(:last-child) { + margin-bottom: 8px; + } + + > input[type="radio"] { + display: none; + + & + .box { + width: 512px; + background: #ffffff; + border: 1px solid #d0d5dd; + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); + border-radius: 8px; + transition: all ease-in-out 250ms; + + & > label { + display: flex; + flex-direction: row; + align-items: center; + font-weight: 400; + font-size: 16px; + line-height: 24px; + color: #667085; + cursor: pointer; + user-select: none; + transition: all ease-in-out 250ms; + + &:before { + content: ""; + display: inline-block; + width: 14px; + height: 14px; + border-radius: 8px; + background: #ffffff; + box-shadow: inset 0 0 0 4px #ffffff; + border: 1px solid #d0d5dd; + margin: 14px 8px 14px 14px; + transition: all ease-in-out 500ms; + } + } + } + + &:checked + .box { + background: #22ccee; + border: 1px solid #d0d5dd; + + & > label { + color: #ffffff; + + &:before { + background: #ffffff; + box-shadow: inset 0 0 0 4px #22ccee; + border: 1px solid #ffffff; + } + } + } + } + } +`; + +export default function Radio({ options, value = undefined, onChange = undefined }) { + const [innerValue, setInnerValue] = useState(0); + const id = useRef(nanoid()); + + return ( + + {options.map((item, index) => ( +
+ { + value === undefined && setInnerValue(index); + onChange !== null && onChange(index); + }} + id={`${id.current}-${index}`} + /> +
+ +
+
+ ))} +
+ ); +} diff --git a/src/routes/index.js b/src/routes/index.js index d98fbe32..58d40f9e 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -21,115 +21,114 @@ import store from "../app/store"; import InvitePage from "./invite"; import SettingPage from "./setting"; import SettingChannelPage from "./settingChannel"; +import OnboardingPage from "./onboarding"; import toast from "react-hot-toast"; import ResourceManagement from "./resources"; const PageRoutes = () => { - const { - ui: { online }, - fileMessages, - } = useSelector((store) => { - return { ui: store.ui, fileMessages: store.fileMessage }; - }); - // 掉线检测 - useEffect(() => { - let toastId = 0; - if (!online) { - toast.error("Network Offline!", { duration: Infinity }); - } else { - toast.dismiss(toastId); - } - }, [online]); + const { + ui: { online }, + fileMessages + } = useSelector((store) => { + return { ui: store.ui, fileMessages: store.fileMessage }; + }); + // 掉线检测 + useEffect(() => { + let toastId = 0; + if (!online) { + toast.error("Network Offline!", { duration: Infinity }); + } else { + toast.dismiss(toastId); + } + }, [online]); - return ( - - - } /> - - - - } - /> - - - - } - /> - - - - } - > - } /> - - } /> - } /> - - - - - - } - /> - } /> - - - - } - > - - } /> - } /> - - } /> - - } /> - } /> - } /> - - - } /> - } /> - - }> - } - > - - } /> - - - ); + return ( + + + } /> + + + + } + /> + + + + } + /> + + + + } + > + } /> + + } /> + } /> + + + + + + } + /> + } /> + } /> + + + + } + > + + } /> + } /> + + } /> + + } /> + } /> + } /> + + + } /> + } /> + + }> + }> + + } /> + + + ); }; // const local_key = "AUTH_DATA"; export default function ReduxRoutes() { - // const [authData, setAuthData] = useState( - // JSON.parse(localStorage.getItem(local_key)) - // ); - // const updateAuthData = (data) => { - // localStorage.setItem(local_key, JSON.stringify(data)); - // setAuthData(data); - // }; - return ( - - - - - ); + // const [authData, setAuthData] = useState( + // JSON.parse(localStorage.getItem(local_key)) + // ); + // const updateAuthData = (data) => { + // localStorage.setItem(local_key, JSON.stringify(data)); + // setAuthData(data); + // }; + return ( + + + + + ); } diff --git a/src/routes/onboarding/index.js b/src/routes/onboarding/index.js new file mode 100644 index 00000000..f530cba3 --- /dev/null +++ b/src/routes/onboarding/index.js @@ -0,0 +1,35 @@ +import { useState } from "react"; +import { Navigate } from "react-router-dom"; +import { Helmet } from "react-helmet"; +import WelcomeStep from "./steps/1-welcome"; +import ServerNameStep from "./steps/2-serverName"; +import AdminCredentialsStep from "./steps/3-adminCredentials"; +import InviteRuleStep from "./steps/4-inviteRule"; +import InviteLinkStep from "./steps/5-inviteLink"; +import CompletedStep from "./steps/6-completed"; +import StyledOnboardingPage from "./styled"; + +export default function OnboardingPage() { + const [step, setStep] = useState(0); + const [data, setData] = useState({ + serverName: "" + }); + const props = { setStep, data, setData }; + + return ( + <> + + Rustchat Setup + + + {step === 0 && } + {step === 1 && } + {step === 2 && } + {step === 3 && } + {step === 4 && } + {step === 5 && } + {step === 6 && } + + + ); +} diff --git a/src/routes/onboarding/steps/1-welcome.js b/src/routes/onboarding/steps/1-welcome.js new file mode 100644 index 00000000..6d274acc --- /dev/null +++ b/src/routes/onboarding/steps/1-welcome.js @@ -0,0 +1,26 @@ +import styled from "styled-components"; +import StyledButton from "../../../common/component/styled/Button"; +import PlayIcon from "../../../assets/icons/play.svg?url"; + +const StyledFirstStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +export default function WelcomeStep({ setStep }) { + return ( + + Welcome to your Rustchat! + + Everything in this space is owned by you. Let’s set up your space! + + setStep((prev) => prev + 1)}> + play icon + Start + + + ); +} diff --git a/src/routes/onboarding/steps/2-serverName.js b/src/routes/onboarding/steps/2-serverName.js new file mode 100644 index 00000000..8fe3d849 --- /dev/null +++ b/src/routes/onboarding/steps/2-serverName.js @@ -0,0 +1,55 @@ +import styled from "styled-components"; +import toast from "react-hot-toast"; +import StyledInput from "../../../common/component/styled/Input"; +import StyledButton from "../../../common/component/styled/Button"; + +const StyledSpaceNameStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .secondaryText { + color: #667085; + } + + > .button { + margin-top: 24px; + } +`; + +export default function ServerNameStep({ setStep, data, setData }) { + return ( + + Create a new server + + Servers are shared environments where teams can work on projects and chat. + + + setData({ + ...data, + serverName: e.target.value + }) + } + /> + { + // Verification for space name + if (data.serverName === "") { + toast.error("Please enter server name!"); + return; + } + setStep((prev) => prev + 1); + }} + > + Create Server + + + ); +} diff --git a/src/routes/onboarding/steps/3-adminCredentials.js b/src/routes/onboarding/steps/3-adminCredentials.js new file mode 100644 index 00000000..f0b507fd --- /dev/null +++ b/src/routes/onboarding/steps/3-adminCredentials.js @@ -0,0 +1,131 @@ +import { useEffect, useState } from "react"; +import styled from "styled-components"; +import toast from "react-hot-toast"; +import { useDispatch } from "react-redux"; +import StyledInput from "../../../common/component/styled/Input"; +import StyledButton from "../../../common/component/styled/Button"; +import { + useCreateAdminMutation, + useGetServerQuery, + useUpdateServerMutation +} from "../../../app/services/server"; +import { useLoginMutation } from "../../../app/services/auth"; +import { updateInitialized } from "../../../app/slices/auth.data"; + +const StyledAdminCredentialsStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .input:not(:nth-last-child(2)) { + margin-bottom: 20px; + } + + > .button { + margin-top: 24px; + } +`; + +export default function AdminCredentialsStep({ data, setStep }) { + const dispatch = useDispatch(); + + const [createAdmin, { isLoading: isSigningUp, error: signUpError }] = useCreateAdminMutation(); + const [login, { isLoading: isLoggingIn, isSuccess: isLoggedIn, error: loginError }] = + useLoginMutation(); + const { data: serverData } = useGetServerQuery(); + const [updateServer, { isLoading: isUpdatingServer, isSuccess: isUpdatedServer }] = + useUpdateServerMutation(); + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [confirm, setConfirm] = useState(""); + + // Display error + useEffect(() => { + if (signUpError === undefined) return; + toast.error(`Failed to sign up: ${signUpError.data}`); + }, [signUpError]); + useEffect(() => { + if (loginError === undefined) return; + toast.error(`Login failed: ${loginError.data}`); + }, [loginError]); + + // After logged in + useEffect(() => { + if (isLoggedIn) { + dispatch(updateInitialized(true)); + setTimeout(() => { + // Set server name + updateServer({ + ...serverData, + name: data.serverName + }); + }, 0); + } + }, [isLoggedIn]); + + // After updated server + useEffect(() => { + if (isUpdatedServer) { + setStep((prev) => prev + 1); + } + }, [isUpdatedServer]); + + return ( + + Now let’s set up your admin account + You are the 1st user and admin of your space! + setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + setConfirm(e.target.value)} + /> + { + // Verification for admin credentials + if (email === "") { + toast.error("Please enter admin email!"); + return; + } else if (password === "") { + toast.error("Please enter admin password!"); + return; + } else if (password !== confirm) { + toast.error("Two passwords do not match!"); + return; + } + await createAdmin({ + email, + name: "Admin", + password, + gender: 0 + }); + await login({ + email, + password, + type: "password" + }); + }} + > + {!(isSigningUp || isLoggingIn || isUpdatingServer) ? "Sign Up" : "..."} + + + ); +} diff --git a/src/routes/onboarding/steps/4-inviteRule.js b/src/routes/onboarding/steps/4-inviteRule.js new file mode 100644 index 00000000..0945e9a0 --- /dev/null +++ b/src/routes/onboarding/steps/4-inviteRule.js @@ -0,0 +1,60 @@ +import { useState, useEffect } from "react"; +import toast from "react-hot-toast"; +import styled from "styled-components"; +import StyledRadio from "../../../common/component/styled/Radio"; +import StyledButton from "../../../common/component/styled/Button"; +import { useGetLoginConfigQuery, useUpdateLoginConfigMutation } from "../../../app/services/server"; + +const StyledInviteRuleStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .input:not(:nth-last-child(2)) { + margin-bottom: 20px; + } +`; + +export default function InviteRuleStep({ setStep }) { + const { data: loginConfig } = useGetLoginConfigQuery(); + const [updateLoginConfig, { isSuccess, error }] = useUpdateLoginConfigMutation(); + + const [value, setValue] = useState(0); + + // Display error + useEffect(() => { + if (error === undefined) return; + toast.error(`Failed to update invitation rule: ${error.data}`); + }, [error]); + + // Increment `step` when updating has completed + useEffect(() => { + if (isSuccess) setStep((prev) => prev + 1); + }, [isSuccess]); + + return ( + + Last step: invite others! + Firstly, who can sign up to this server? + { + setValue(v); + if (loginConfig !== undefined) { + const whoCanSignUp = ["EveryOne", "InvitationOnly"][v]; + await updateLoginConfig({ + ...loginConfig, + who_can_sign_up: whoCanSignUp + }); + } + }} + /> + setStep((prev) => prev + 1)}> + Skip + + + ); +} diff --git a/src/routes/onboarding/steps/5-inviteLink.js b/src/routes/onboarding/steps/5-inviteLink.js new file mode 100644 index 00000000..14e79c85 --- /dev/null +++ b/src/routes/onboarding/steps/5-inviteLink.js @@ -0,0 +1,73 @@ +import styled from "styled-components"; +import StyledInput from "../../../common/component/styled/Input"; +import StyledButton from "../../../common/component/styled/Button"; +import useInviteLink from "../../../common/hook/useInviteLink"; + +const StyledInviteLinkStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .secondaryText { + margin-bottom: 40px; + } + + > .tip { + font-weight: 600; + font-size: 14px; + line-height: 20px; + color: #475467; + margin-bottom: 8px; + } + + > .link { + position: relative; + background: #ffffff; + border: 1px solid #f4f4f5; + box-shadow: 0 1px 2px rgba(31, 41, 55, 0.08); + border-radius: 4px; + width: 374px; + display: flex; + + > input { + border: none; + box-shadow: none; + padding: 11px 0 11px 8px; + font-weight: 400; + font-size: 14px; + line-height: 20px; + color: #78787c; + } + + > button { + padding: 0 8px; + font-weight: 500; + font-size: 14px; + line-height: 20px; + color: #22ccee; + } + } +`; + +export default function InviteLinkStep({ setStep }) { + const { link, linkCopied, copyLink } = useInviteLink(); + + return ( + + Last step: invite others! + Now let’s invite others! + Send invitation link to your future community members: +
+ + + {linkCopied ? "Copied" : `Copy`} + +
+ setStep((prev) => prev + 1)}> + Skip + +
+ ); +} diff --git a/src/routes/onboarding/steps/6-completed.js b/src/routes/onboarding/steps/6-completed.js new file mode 100644 index 00000000..616b23b7 --- /dev/null +++ b/src/routes/onboarding/steps/6-completed.js @@ -0,0 +1,44 @@ +import styled from "styled-components"; +import StyledButton from "../../../common/component/styled/Button"; +import PlayIcon from "../../../assets/icons/play.svg?url"; + +const StyledLastStep = styled.div` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + > .secondaryText { + margin-bottom: 48px; + } + + > .tip { + width: 560px; + font-size: 20px; + line-height: 24px; + text-align: center; + margin-bottom: 96px; + + > .strong { + font-weight: 700; + } + } +`; + +export default function CompletedStep({ data, setStep }) { + return ( + + Welcome to {data.serverName} + Proudly presented by Rustchat + + More settings, including domain resolution, privileges, securities, and invites are available in{" "} + Settings + + setStep((prev) => prev + 1)}> + play icon + Enter + + + ); +} diff --git a/src/routes/onboarding/styled.js b/src/routes/onboarding/styled.js new file mode 100644 index 00000000..bcf409ad --- /dev/null +++ b/src/routes/onboarding/styled.js @@ -0,0 +1,75 @@ +import styled from "styled-components"; + +const StyledOnboardingPage = styled.div` + height: 100vh; + overflow-y: auto; + + // shared with child components + .primaryText, + .secondaryText { + text-align: center; + } + + .primaryText { + font-weight: 700; + font-size: 24px; + line-height: 30px; + margin-bottom: 8px; + } + + .secondaryText { + 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; + } + } + + .input { + width: 360px; + height: 44px; + border: none; + box-shadow: none; + } + + input.input { + 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; + + &.ghost.border_less { + width: fit-content; + font-weight: 500; + font-size: 16px; + line-height: 24px; + color: #98a2b3; + margin-top: 14px; + } + } +`; + +export default StyledOnboardingPage; diff --git a/yarn.lock b/yarn.lock index 1155338e..c4985f88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9298,7 +9298,7 @@ utils-merge@1.0.1: uuid@^8.3.2: version "8.3.2" - resolved "https://mirrors.tencent.com/npm/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== v8-compile-cache-lib@^3.0.0: