diff --git a/i18n/en/common.json b/i18n/en/common.json index cbb07ea5..cafebcc0 100644 --- a/i18n/en/common.json +++ b/i18n/en/common.json @@ -46,5 +46,14 @@ }, "next": "Next", "know_more": "Know more", - "ago": "ago" + "ago": "ago", + "signin": "Signin", + "subscribe": "My Subscribe", + "profile_setting": "Account Settings", + "signout": "SignOut", + "btn": { + "save": "Save", + "cancel": "Cancel", + "func_disabled": "The feature is temporarily disabled" + } } diff --git a/i18n/en/setting.json b/i18n/en/setting.json new file mode 100644 index 00000000..7f1190f7 --- /dev/null +++ b/i18n/en/setting.json @@ -0,0 +1,28 @@ +{ + "profile": { + "avatar": "Avatar", + "userinfo": "Profile", + "account_settings": "Account Settings", + "form": { + "name": "name", + "name_placeholder": "Please enter your name", + "email": "email", + "email_ques_icon": "The email is used to receive project update notifications.", + "email_placeholder": "Please enter your email address", + "error_require": "{{field}} is a required field", + "error_name_max_len": "nickname cannot exceed {{length}} characters", + "error_email_format": "email format is incorrect" + }, + "connected_accounts": "Connected Accounts", + "connected": "Connected", + "disconnect": "Disconnect", + "connect_multiple_accounts_to_your_user_and_sign_in": "Connect multiple accounts to your user and sign in with any of them", + "delete_account": "Delete account", + "delete_account_btn": "Delete account", + "delete_account_warning": " Once you delete your account, there is no going back. Please be certain when taking this action.", + "can_be_used_to_submit_project_after_binding": "Can be used to submit project after binding", + "verified": "Verified", + "unverified_yet": "Unverified yet,", + "resend_verification_email": "resend verification email" + } +} diff --git a/i18n/en/submit_project.json b/i18n/en/submit_project.json index 625c0578..b3fb7e81 100644 --- a/i18n/en/submit_project.json +++ b/i18n/en/submit_project.json @@ -6,7 +6,8 @@ "submit_your_project": "Submit your project", "single_repository": "Single repository", "your_project_hosting_on": "Your project hosting on", - "logout": "Logout", + "switch_gitee": "switch to Gitee account", + "switch_github": "switch to GitHub account", "select_your_own_repository_on": "Select your own repository on {{providerName}}", "type_the_address_of_any_repository": "Type the address of any repository", "pick_your_own_repository_on": "Pick your own repository on {{providerName}}", @@ -48,7 +49,7 @@ "by_creating_an_account": "By creating an account, you agree to the", "as_well_as": "as well as", "terms_of_use": " Terms of Use ", - "non_active_account_processing_specification": " Non-active Account Processing Specification.", + "privacy_policy": " Non-active Account Processing Specification.", "email_verification_successful": "Email verification successful", "the_email_address": "The email address", "has_been_successfully": "has been successfully bound to your OSS Compass account", diff --git a/i18n/zh/common.json b/i18n/zh/common.json index 7f2b7470..8efc30a4 100644 --- a/i18n/zh/common.json +++ b/i18n/zh/common.json @@ -46,5 +46,13 @@ }, "next": "下一个", "know_more": "了解更多", - "ago": "前" + "ago": "前", + "signin": "登录", + "profile_setting": "账号设置", + "signout": "退出", + "btn": { + "save": "保存", + "cancel": "取消", + "func_disabled": "该功能临时关闭" + } } diff --git a/i18n/zh/setting.json b/i18n/zh/setting.json new file mode 100644 index 00000000..23ac6e01 --- /dev/null +++ b/i18n/zh/setting.json @@ -0,0 +1,28 @@ +{ + "profile": { + "avatar": "头像", + "userinfo": "用户信息", + "account_settings": "账号设置", + "form": { + "name": "昵称", + "name_placeholder": "请输入昵称", + "email": "邮箱", + "email_ques_icon": "该电子邮件用于接收项目更新通知", + "email_placeholder": "请输入邮箱地址", + "error_require": "{{field}}不能为空", + "error_name_max_len": "昵称长度不能超过{{length}}", + "error_email_format": "邮件格式不正确" + }, + "connected_accounts": "第三方绑定", + "connected": "绑定", + "disconnect": "解绑", + "connect_multiple_accounts_to_your_user_and_sign_in": "绑定第三方账号,即可使用任何一个账户进行登录。", + "delete_account": "删除账号", + "delete_account_btn": "删除我的账号", + "delete_account_warning": "删除账户后,就无法进行撤销。在执行此操作时,请确保您已经仔细考虑过。", + "can_be_used_to_submit_project_after_binding": "绑定后可用于提交项目", + "verified": "已验证", + "unverified_yet": "未验证,", + "resend_verification_email": "发送验证邮件" + } +} diff --git a/i18n/zh/submit_project.json b/i18n/zh/submit_project.json index 54811528..54e6f2c6 100644 --- a/i18n/zh/submit_project.json +++ b/i18n/zh/submit_project.json @@ -48,7 +48,7 @@ "by_creating_an_account": "要创建一个帐户,您同意", "as_well_as": "以及", "terms_of_use": "使用条款", - "non_active_account_processing_specification": "非活跃帐户处理规范。", + "privacy_policy": "隐私协议", "email_verification_successful": "邮件验证成功", "the_email_address": "已成功将邮箱地址", "has_been_successfully": "与您的 OSS Compass 帐户绑定", diff --git a/next.config.js b/next.config.js index 7c72be5d..98f2793f 100644 --- a/next.config.js +++ b/next.config.js @@ -22,7 +22,11 @@ const nextConfig = { includePaths: [path.join(__dirname, 'src/styles')], }, images: { - domains: ['portrait.gitee.com', 'avatars.githubusercontent.com'], + domains: [ + 'portrait.gitee.com', + 'foruda.gitee.com', + 'avatars.githubusercontent.com', + ], }, webpack(config) { config.module.rules.push({ diff --git a/package.json b/package.json index db48c473..1b009cb1 100644 --- a/package.json +++ b/package.json @@ -53,10 +53,12 @@ "qrcode": "^1.5.1", "query-string": "^7.1.1", "react": "18.2.0", + "react-cropper": "^2.3.3", "react-datepicker": "^4.11.0", "react-dom": "18.2.0", "react-error-boundary": "^3.1.4", "react-hook-form": "^7.36.0", + "react-hot-toast": "^2.4.1", "react-hotkeys-hook": "^3.4.7", "react-i18next": "^12.0.0", "react-icons": "^4.4.0", diff --git a/src/common/components/Button.tsx b/src/common/components/Button.tsx index b0af23f2..9615bd90 100644 --- a/src/common/components/Button.tsx +++ b/src/common/components/Button.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren } from 'react'; +import React, { ReactNode, forwardRef } from 'react'; import classnames from 'classnames'; import { cva, type VariantProps } from 'class-variance-authority'; import { twMerge } from 'tailwind-merge'; @@ -21,6 +21,7 @@ const buttonVariants = cva( secondary: 'border border-black text-black font-bold hover:bg-gray-100', danger: 'border-2 border-[#CC0000] text-[#CC0000] font-bold hover:bg-red-600/5', + text: '', }, size: { lg: 'text-base px-10 py-3', @@ -37,17 +38,22 @@ const buttonVariants = cva( interface ButtonVariants extends VariantProps {} -const Button: React.FC> = ({ - children, - disabled = false, - loading = false, - type = 'button', - intent, - size, - className, - onClick, - ...props -}) => { +const Button = forwardRef< + HTMLButtonElement, + ButtonProps & ButtonVariants & { children?: ReactNode | undefined } +>((props, ref) => { + const { + children, + disabled = false, + loading = false, + type = 'button', + intent, + size, + className, + onClick, + ...restProps + } = props; + const cls = classnames( buttonVariants({ intent, size }), { 'opacity-50 cursor-not-allowed hover:opacity-50': disabled }, @@ -56,6 +62,7 @@ const Button: React.FC> = ({ return ( ); -}; +}); + +Button.displayName = 'Button'; export default Button; diff --git a/src/common/components/Header/User.tsx b/src/common/components/Header/User.tsx new file mode 100644 index 00000000..8a86a4fe --- /dev/null +++ b/src/common/components/Header/User.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import Image from 'next/image'; +import { AiOutlineUser } from 'react-icons/ai'; +import { MdOutlineLogout } from 'react-icons/md'; +import client from '@graphql/client'; +import { useSignOutMutation } from '@graphql/generated'; +import { resetUserInfo } from '@modules/auth/UserInfoStore'; +import { useTranslation } from 'react-i18next'; +import useProviderInfo from '@modules/auth/useProviderInfo'; + +const User = () => { + const { t } = useTranslation(); + const router = useRouter(); + const mutation = useSignOutMutation(client); + const { providerUser: user } = useProviderInfo(); + + if (!user) { + return ( + + {t('common:signin')} + + ); + } + + return ( +
+
+ +
+ +
+
+ {/**/} + {/* */} + {/* {t('common:subscribe')}*/} + {/* */} + {/**/} + + + + + {t('common:profile_setting')} + + + +
{ + mutation.mutate( + {}, + { + onSuccess: () => { + resetUserInfo(); + router.push('/auth/signin'); + }, + } + ); + }} + > + {t('common:signout')} +
+
+
+
+ ); +}; + +export default User; diff --git a/src/common/components/Header/index.tsx b/src/common/components/Header/index.tsx index 0474963d..7b77deb3 100644 --- a/src/common/components/Header/index.tsx +++ b/src/common/components/Header/index.tsx @@ -8,6 +8,7 @@ import MobileHeader from './MobileHeader'; import CommunityDropdown from './CommunityDropdown'; import ChangeLanguage from './ChangeLanguage'; import SubmitYouProject from './SubmitYouProject'; +import User from './User'; const Header: React.FC<{ sticky?: boolean; @@ -60,6 +61,7 @@ const Header: React.FC<{
+
diff --git a/src/common/components/Input.tsx b/src/common/components/Input.tsx index eed1c1a4..50626e56 100644 --- a/src/common/components/Input.tsx +++ b/src/common/components/Input.tsx @@ -8,9 +8,8 @@ const Input = forwardRef< value?: string; onChange?: (e: ChangeEvent) => void; onBlur?: (e: FocusEvent) => void; - defaultValue?: string; className?: string; - placeholder: string; + placeholder?: string; error?: boolean; disabled?: boolean; } @@ -18,7 +17,6 @@ const Input = forwardRef< const { name, className, - defaultValue, value, onChange, onBlur, @@ -42,7 +40,7 @@ const Input = forwardRef< disabled={disabled} className={classnames( className, - 'daisy-input-bordered daisy-input h-12 flex-1 border-2 px-4 text-base outline-none', + 'daisy-input-bordered daisy-input h-12 w-full flex-1 border-2 px-4 text-base outline-none', [error ? 'border-red-500' : 'border-black'] )} /> diff --git a/src/common/components/Layout/Center.tsx b/src/common/components/Layout/Center.tsx index 2ab82c2b..5d590e24 100644 --- a/src/common/components/Layout/Center.tsx +++ b/src/common/components/Layout/Center.tsx @@ -1,12 +1,13 @@ import React, { PropsWithChildren } from 'react'; import classnames from 'classnames'; -const Center: React.FC> = ({ - children, - className, -}) => { +const Center: React.FC< + PropsWithChildren<{ className?: string; widthClassName?: string }> +> = ({ children, className, widthClassName = 'w-[1200px]' }) => { return ( -
+
{children}
); diff --git a/src/graphql/generated.ts b/src/graphql/generated.ts index 3de1fd67..d153b41e 100644 --- a/src/graphql/generated.ts +++ b/src/graphql/generated.ts @@ -480,6 +480,27 @@ export type MetricStat = { median?: Maybe; }; +/** Autogenerated input type of ModifyUser */ +export type ModifyUserInput = { + /** A unique identifier for the client performing the mutation. */ + clientMutationId?: InputMaybe; + /** user email */ + email: Scalars['String']; + /** user name */ + name: Scalars['String']; +}; + +/** Autogenerated return type of ModifyUser */ +export type ModifyUserPayload = { + __typename?: 'ModifyUserPayload'; + /** A unique identifier for the client performing the mutation. */ + clientMutationId?: Maybe; + /** Errors encountered during execution of the mutation. */ + errors?: Maybe>; + message?: Maybe; + status: Scalars['String']; +}; + export type Mutation = { __typename?: 'Mutation'; /** Submit a community analysis task */ @@ -488,8 +509,14 @@ export type Mutation = { createRepoTask?: Maybe; /** Destroy user */ destroyUser?: Maybe; + /** Modify user */ + modifyUser?: Maybe; + /** Send email verify */ + sendEmailVerify?: Maybe; /** Sign out */ signOut?: Maybe; + /** User unbind */ + userUnbind?: Maybe; }; export type MutationCreateProjectTaskArgs = { @@ -500,6 +527,18 @@ export type MutationCreateRepoTaskArgs = { input: CreateRepoTaskInput; }; +export type MutationModifyUserArgs = { + input: ModifyUserInput; +}; + +export type MutationSendEmailVerifyArgs = { + input: SendEmailVerifyInput; +}; + +export type MutationUserUnbindArgs = { + input: UserUnbindInput; +}; + export type ProjectCompletionRow = { __typename?: 'ProjectCompletionRow'; /** metric model object identification */ @@ -682,6 +721,23 @@ export type Repo = { watchersCount?: Maybe; }; +/** Autogenerated input type of SendEmailVerify */ +export type SendEmailVerifyInput = { + /** A unique identifier for the client performing the mutation. */ + clientMutationId?: InputMaybe; +}; + +/** Autogenerated return type of SendEmailVerify */ +export type SendEmailVerifyPayload = { + __typename?: 'SendEmailVerifyPayload'; + /** A unique identifier for the client performing the mutation. */ + clientMutationId?: Maybe; + /** Errors encountered during execution of the mutation. */ + errors?: Maybe>; + message?: Maybe; + status: Scalars['String']; +}; + export type StarterProjectHealthMetric = { __typename?: 'StarterProjectHealthMetric'; /** the smallest number of people that make 50% of contributions */ @@ -740,7 +796,30 @@ export type Trending = { export type User = { __typename?: 'User'; + email: Scalars['String']; + emailVerified: Scalars['Boolean']; + id: Scalars['Int']; loginBinds?: Maybe>; + name: Scalars['String']; +}; + +/** Autogenerated input type of UserUnbind */ +export type UserUnbindInput = { + /** A unique identifier for the client performing the mutation. */ + clientMutationId?: InputMaybe; + /** provider name */ + provider: Scalars['String']; +}; + +/** Autogenerated return type of UserUnbind */ +export type UserUnbindPayload = { + __typename?: 'UserUnbindPayload'; + /** A unique identifier for the client performing the mutation. */ + clientMutationId?: Maybe; + /** Errors encountered during execution of the mutation. */ + errors?: Maybe>; + message?: Maybe; + status: Scalars['String']; }; export type CreateRepoTaskMutationVariables = Exact<{ @@ -776,6 +855,44 @@ export type CreateProjectTaskMutation = { } | null; }; +export type ModifyUserMutationVariables = Exact<{ + name: Scalars['String']; + email: Scalars['String']; +}>; + +export type ModifyUserMutation = { + __typename?: 'Mutation'; + modifyUser?: { + __typename?: 'ModifyUserPayload'; + message?: string | null; + status: string; + } | null; +}; + +export type UserUnbindMutationVariables = Exact<{ + provider: Scalars['String']; +}>; + +export type UserUnbindMutation = { + __typename?: 'Mutation'; + userUnbind?: { + __typename?: 'UserUnbindPayload'; + message?: string | null; + status: string; + } | null; +}; + +export type SendEmailVerifyMutationVariables = Exact<{ [key: string]: never }>; + +export type SendEmailVerifyMutation = { + __typename?: 'Mutation'; + sendEmailVerify?: { + __typename?: 'SendEmailVerifyPayload'; + message?: string | null; + status: string; + } | null; +}; + export type SignOutMutationVariables = Exact<{ [key: string]: never }>; export type SignOutMutation = { @@ -783,12 +900,23 @@ export type SignOutMutation = { signOut?: boolean | null; }; +export type DeleteUserMutationVariables = Exact<{ [key: string]: never }>; + +export type DeleteUserMutation = { + __typename?: 'Mutation'; + destroyUser?: boolean | null; +}; + export type UserinfoQueryVariables = Exact<{ [key: string]: never }>; export type UserinfoQuery = { __typename?: 'Query'; currentUser?: { __typename?: 'User'; + id: number; + name: string; + email: string; + emailVerified: boolean; loginBinds?: Array<{ __typename?: 'LoginBind'; account?: string | null; @@ -1510,6 +1638,144 @@ useCreateProjectTaskMutation.fetcher = ( variables, headers ); +export const ModifyUserDocument = /*#__PURE__*/ ` + mutation modifyUser($name: String!, $email: String!) { + modifyUser(input: {name: $name, email: $email}) { + message + status + } +} + `; +export const useModifyUserMutation = ( + client: GraphQLClient, + options?: UseMutationOptions< + ModifyUserMutation, + TError, + ModifyUserMutationVariables, + TContext + >, + headers?: RequestInit['headers'] +) => + useMutation< + ModifyUserMutation, + TError, + ModifyUserMutationVariables, + TContext + >( + ['modifyUser'], + (variables?: ModifyUserMutationVariables) => + fetcher( + client, + ModifyUserDocument, + variables, + headers + )(), + options + ); +useModifyUserMutation.fetcher = ( + client: GraphQLClient, + variables: ModifyUserMutationVariables, + headers?: RequestInit['headers'] +) => + fetcher( + client, + ModifyUserDocument, + variables, + headers + ); +export const UserUnbindDocument = /*#__PURE__*/ ` + mutation userUnbind($provider: String!) { + userUnbind(input: {provider: $provider}) { + message + status + } +} + `; +export const useUserUnbindMutation = ( + client: GraphQLClient, + options?: UseMutationOptions< + UserUnbindMutation, + TError, + UserUnbindMutationVariables, + TContext + >, + headers?: RequestInit['headers'] +) => + useMutation< + UserUnbindMutation, + TError, + UserUnbindMutationVariables, + TContext + >( + ['userUnbind'], + (variables?: UserUnbindMutationVariables) => + fetcher( + client, + UserUnbindDocument, + variables, + headers + )(), + options + ); +useUserUnbindMutation.fetcher = ( + client: GraphQLClient, + variables: UserUnbindMutationVariables, + headers?: RequestInit['headers'] +) => + fetcher( + client, + UserUnbindDocument, + variables, + headers + ); +export const SendEmailVerifyDocument = /*#__PURE__*/ ` + mutation sendEmailVerify { + sendEmailVerify(input: {}) { + message + status + } +} + `; +export const useSendEmailVerifyMutation = < + TError = unknown, + TContext = unknown +>( + client: GraphQLClient, + options?: UseMutationOptions< + SendEmailVerifyMutation, + TError, + SendEmailVerifyMutationVariables, + TContext + >, + headers?: RequestInit['headers'] +) => + useMutation< + SendEmailVerifyMutation, + TError, + SendEmailVerifyMutationVariables, + TContext + >( + ['sendEmailVerify'], + (variables?: SendEmailVerifyMutationVariables) => + fetcher( + client, + SendEmailVerifyDocument, + variables, + headers + )(), + options + ); +useSendEmailVerifyMutation.fetcher = ( + client: GraphQLClient, + variables?: SendEmailVerifyMutationVariables, + headers?: RequestInit['headers'] +) => + fetcher( + client, + SendEmailVerifyDocument, + variables, + headers + ); export const SignOutDocument = /*#__PURE__*/ ` mutation signOut { signOut @@ -1547,9 +1813,55 @@ useSignOutMutation.fetcher = ( variables, headers ); +export const DeleteUserDocument = /*#__PURE__*/ ` + mutation deleteUser { + destroyUser +} + `; +export const useDeleteUserMutation = ( + client: GraphQLClient, + options?: UseMutationOptions< + DeleteUserMutation, + TError, + DeleteUserMutationVariables, + TContext + >, + headers?: RequestInit['headers'] +) => + useMutation< + DeleteUserMutation, + TError, + DeleteUserMutationVariables, + TContext + >( + ['deleteUser'], + (variables?: DeleteUserMutationVariables) => + fetcher( + client, + DeleteUserDocument, + variables, + headers + )(), + options + ); +useDeleteUserMutation.fetcher = ( + client: GraphQLClient, + variables?: DeleteUserMutationVariables, + headers?: RequestInit['headers'] +) => + fetcher( + client, + DeleteUserDocument, + variables, + headers + ); export const UserinfoDocument = /*#__PURE__*/ ` query userinfo { currentUser { + id + name + email + emailVerified loginBinds { account avatarUrl diff --git a/src/graphql/mutation.graphql b/src/graphql/mutation.graphql index 48f23177..d36e020b 100644 --- a/src/graphql/mutation.graphql +++ b/src/graphql/mutation.graphql @@ -26,6 +26,31 @@ mutation createProjectTask( } } +mutation modifyUser($name: String!, $email: String!) { + modifyUser(input: { name: $name, email: $email }) { + message + status + } +} + +mutation userUnbind($provider: String!) { + userUnbind(input: { provider: $provider }) { + message + status + } +} + +mutation sendEmailVerify { + sendEmailVerify(input: {}) { + message + status + } +} + mutation signOut { signOut } + +mutation deleteUser { + destroyUser +} diff --git a/src/graphql/query.graphql b/src/graphql/query.graphql index d73f7a75..9a1b34f3 100644 --- a/src/graphql/query.graphql +++ b/src/graphql/query.graphql @@ -1,5 +1,9 @@ query userinfo { currentUser { + id + name + email + emailVerified loginBinds { account avatarUrl @@ -373,6 +377,7 @@ query summary($start: ISO8601DateTime, $end: ISO8601DateTime) { # } # } } + query labMetric( $label: String! $level: String = "repo" @@ -404,6 +409,7 @@ query labMetric( type } } + fragment metricStat on MetricStat { mean median diff --git a/src/modules/auth/AuthRequire.tsx b/src/modules/auth/AuthRequire.tsx new file mode 100644 index 00000000..7b546737 --- /dev/null +++ b/src/modules/auth/AuthRequire.tsx @@ -0,0 +1,58 @@ +import React, { PropsWithChildren } from 'react'; +import router from 'next/router'; +import { useSnapshot } from 'valtio'; +import { userInfoStore } from './UserInfoStore'; + +interface Props { + className?: string; + loadingUi?: React.ReactNode; + redirectTo?: string; +} + +const AuthRequire: React.FC> = ({ + children, + className, + loadingUi, + redirectTo, +}) => { + const { currentUser, loading } = useSnapshot(userInfoStore); + + if (!loading && !currentUser) { + let redirectUrl = redirectTo ?? window.location.pathname; + router.replace( + `/auth/signin?redirect_to=${encodeURIComponent(redirectUrl)}` + ); + } + + if (loading && loadingUi) { + return <>{loadingUi}; + } + + if (loading) { + return ( +
+
+
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+
+
+ ); + } + + return <>{children}; +}; + +export default AuthRequire; diff --git a/src/modules/auth/LoginCard.tsx b/src/modules/auth/LoginCard.tsx deleted file mode 100644 index 1af41c03..00000000 --- a/src/modules/auth/LoginCard.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'next-i18next'; -import classnames from 'classnames'; -import Image from 'next/image'; -import { setCallbackUrl, setAuthProvider } from '@common/utils/cookie'; - -const LoginCard: React.FC<{ provider: { id: string; name: string } }> = ({ - provider, -}) => { - const { t } = useTranslation(); - if (provider.id === 'github') { - return ( -
{ - setAuthProvider('github'); - setCallbackUrl('/submit-your-project'); - window.location.href = '/users/auth/github'; - }} - > -
- {'github'} -
-
- {t('submit_project:sign_in_with_github')} -
-
- ); - } - - if (provider.id === 'gitee') { - return ( -
{ - setAuthProvider('gitee'); - setCallbackUrl('/submit-your-project'); - window.location.href = '/users/auth/gitee'; - }} - > -
- {'gitee'} -
-

- {t('submit_project:sign_in_with_gitee')} -

-
- ); - } - - return null; -}; - -export default LoginCard; diff --git a/src/modules/auth/LoginItems.tsx b/src/modules/auth/LoginItems.tsx new file mode 100644 index 00000000..4c5a63a8 --- /dev/null +++ b/src/modules/auth/LoginItems.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useRouter } from 'next/router'; +import Image from 'next/image'; +import { useTranslation } from 'next-i18next'; +import client from '@graphql/client'; +import { useSignOutMutation } from '@graphql/generated'; +import { setCallbackUrl, setAuthProvider } from '@common/utils/cookie'; + +const LoginItems: React.FC = () => { + const router = useRouter(); + const { t } = useTranslation(); + const mutation = useSignOutMutation(client); + + const redirectTo = React.useMemo(() => { + const rt = router.query.redirect_to; + return typeof rt === 'string' ? rt : '/settings/profile'; + }, [router.query.redirect_to]); + + return ( + <> +
{ + mutation.mutate( + {}, + { + onSuccess: () => { + setAuthProvider('github'); + setCallbackUrl(redirectTo); + window.location.href = '/users/auth/github'; + }, + } + ); + }} + > + {'github'} + {t('submit_project:continue_with_github')} +
+ +
{ + mutation.mutate( + {}, + { + onSuccess: () => { + setAuthProvider('gitee'); + setCallbackUrl(redirectTo); + window.location.href = '/users/auth/gitee'; + }, + } + ); + }} + > + {'gitee'} + {t('submit_project:continue_with_gitee')} +
+ + ); +}; + +export default LoginItems; diff --git a/src/modules/auth/LoginPage.tsx b/src/modules/auth/LoginPage.tsx deleted file mode 100644 index 58dde0b9..00000000 --- a/src/modules/auth/LoginPage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useEffect } from 'react'; -import { useTranslation } from 'next-i18next'; -import { FiAlertCircle } from 'react-icons/fi'; -import { useRouter } from 'next/router'; -import { oauthProvider } from '@common/constant'; -import LoginCard from './LoginCard'; - -const LoginPage: React.FC = () => { - const { t } = useTranslation(); - const router = useRouter(); - const error = router.query.error; - - return ( -
-

- {t('submit_project:please_select_the_platform_where_your_project_is_h')} -

-
-
- {Object.values(oauthProvider).map((provider) => { - return ; - })} -
-
- {error && ( -

- {error} -

- )} -
- ); -}; - -export default LoginPage; diff --git a/src/modules/auth/UserInfoContext.tsx b/src/modules/auth/UserInfoContext.tsx deleted file mode 100644 index 97b29982..00000000 --- a/src/modules/auth/UserInfoContext.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { createContext, useContext, PropsWithChildren } from 'react'; -import client from '@graphql/client'; -import { useUserinfoQuery, UserinfoQuery } from '@graphql/generated'; -import { getAuthProvider } from '@common/utils/cookie'; - -const UserContext = createContext(undefined); - -export const UserInfoProvider: React.FC = ({ children }) => { - const { data, isLoading } = useUserinfoQuery(client); - if (isLoading) { - return null; - } - - return {children}; -}; - -export const useUserInfo = () => { - const ctx = useContext(UserContext); - - let user; - const provider = getAuthProvider(); - if (provider) { - user = ctx?.currentUser?.loginBinds?.find( - (bindInfo) => bindInfo.provider === provider - ); - } else { - user = ctx?.currentUser?.loginBinds?.[0]; - } - - if (user) { - user = { - ...user, - // todo Let the backend modify - // The naming of the returned fields in the interface data is reversed. - account: user?.nickname, - nickname: user?.account, - }; - } - - return { user, ...ctx }; -}; diff --git a/src/modules/auth/UserInfoFetcher.ts b/src/modules/auth/UserInfoFetcher.ts new file mode 100644 index 00000000..f4acb5e0 --- /dev/null +++ b/src/modules/auth/UserInfoFetcher.ts @@ -0,0 +1,37 @@ +import React, { PropsWithChildren, useEffect } from 'react'; +import client from '@graphql/client'; +import { useEventEmitter } from 'ahooks'; +import { useUserinfoQuery } from '@graphql/generated'; +import { + serUserLoading, + setUserInfo, + userInfoStore, + UserEventType, + userEvent, +} from './UserInfoStore'; + +const UserInfoFetcher: React.FC = ({ children }) => { + const { data, isLoading, refetch } = useUserinfoQuery(client, {}); + + const event$ = useEventEmitter(); + event$.useSubscription((e) => { + if (e === userEvent.REFRESH) { + refetch(); + } + }); + useEffect(() => { + userInfoStore.event$ = event$; + }, [event$]); + + useEffect(() => { + setUserInfo(data); + }, [data]); + + useEffect(() => { + serUserLoading(isLoading); + }, [isLoading]); + + return null; +}; + +export default UserInfoFetcher; diff --git a/src/modules/auth/UserInfoStore.tsx b/src/modules/auth/UserInfoStore.tsx new file mode 100644 index 00000000..16c09ceb --- /dev/null +++ b/src/modules/auth/UserInfoStore.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { proxy, ref } from 'valtio'; +import { UserinfoQuery } from '@graphql/generated'; +import { EventEmitter } from 'ahooks/lib/useEventEmitter'; + +export const userEvent = { + REFRESH: 'refresh' as const, +}; + +export type UserEventType = typeof userEvent[keyof typeof userEvent]; + +export const userInfoStore = proxy<{ + loading: boolean; + currentUser: UserinfoQuery['currentUser'] | null; + event$: EventEmitter | null; +}>({ + loading: true, + currentUser: null, + event$: null, +}); + +export const setUserInfo = (res?: UserinfoQuery) => { + userInfoStore.currentUser = res?.currentUser || null; +}; + +export const serUserLoading = (loading: boolean) => { + userInfoStore.loading = loading; +}; + +export const resetUserInfo = () => { + userInfoStore.loading = false; + userInfoStore.currentUser = null; +}; diff --git a/src/modules/auth/components/LogoHeader.tsx b/src/modules/auth/components/LogoHeader.tsx index e1ba8641..bb0742b6 100644 --- a/src/modules/auth/components/LogoHeader.tsx +++ b/src/modules/auth/components/LogoHeader.tsx @@ -1,10 +1,18 @@ import React from 'react'; +import router from 'next/router'; import Logo from '@public/images/logos/compass-logo.svg'; const LogoHeader = () => { return (
- +
{ + router.push('/'); + }} + > + +
); }; diff --git a/src/modules/auth/useProviderInfo.tsx b/src/modules/auth/useProviderInfo.tsx new file mode 100644 index 00000000..4b16cc7e --- /dev/null +++ b/src/modules/auth/useProviderInfo.tsx @@ -0,0 +1,64 @@ +import React, { useEffect } from 'react'; +import { useSnapshot } from 'valtio'; +import { useToggle } from 'ahooks'; +import { UserinfoQuery } from '@graphql/generated'; +import { getAuthProvider, setAuthProvider } from '@common/utils/cookie'; +import { userInfoStore } from '@modules/auth/UserInfoStore'; +import { ReadonlyDeep } from 'type-fest'; + +type LoginBinds = ReadonlyDeep< + NonNullable['loginBinds'] +>; + +function findSpecifyProvider({ + loginBinds, + provider, +}: { + loginBinds?: LoginBinds; + provider?: string; +}) { + let providerUser; + + if (provider && loginBinds && loginBinds.length > 1) { + providerUser = loginBinds?.find( + (bindInfo) => bindInfo.provider === provider + ); + } else { + providerUser = loginBinds?.[0]; + } + + if (providerUser) { + providerUser = { + ...providerUser, + // todo Let the backend modify + // The naming of the returned fields in the interface data is reversed. + account: providerUser?.nickname, + nickname: providerUser?.account, + }; + } + + return providerUser || null; +} + +const toggleProviders = ['github', 'gitee']; +const getAnother = (p?: string) => toggleProviders.filter((i) => i !== p)[0]; + +const useProviderInfo = () => { + const { currentUser: user } = useSnapshot(userInfoStore); + + const login = getAuthProvider() || 'github'; + const [provider, { toggle }] = useToggle(login, getAnother(login)); + + useEffect(() => { + setAuthProvider(provider); + }, [provider]); + + const showUser = findSpecifyProvider({ + provider: provider, + loginBinds: user?.loginBinds, + }); + + return { providerUser: showUser, loginBinds: user?.loginBinds, toggle }; +}; + +export default useProviderInfo; diff --git a/src/modules/settings/profile/DeleteAccount.tsx b/src/modules/settings/profile/DeleteAccount.tsx new file mode 100644 index 00000000..7e03bdea --- /dev/null +++ b/src/modules/settings/profile/DeleteAccount.tsx @@ -0,0 +1,88 @@ +import React, { useEffect } from 'react'; +import router from 'next/router'; +import client from '@graphql/client'; +import { useTranslation } from 'next-i18next'; +import { useDeleteUserMutation } from '@graphql/generated'; +import Button from '@common/components/Button'; +import { userInfoStore, userEvent } from '@modules/auth/UserInfoStore'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; + +const DeleteAccount = () => { + const { t } = useTranslation(); + const mutation = useDeleteUserMutation(client); + const [open, setOpen] = React.useState(false); + + const handleClose = () => { + setOpen(false); + }; + + return ( + <> +
+ {t('setting:profile.delete_account')} +
+
{t('setting:profile.delete_account_warning')}
+ + + + + {t('setting:profile.delete_account')} + + + + {t('setting:profile.delete_account_warning')} + + + + + + + + + ); +}; + +export default DeleteAccount; diff --git a/src/modules/settings/profile/OAuthList.tsx b/src/modules/settings/profile/OAuthList.tsx new file mode 100644 index 00000000..deda5327 --- /dev/null +++ b/src/modules/settings/profile/OAuthList.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSnapshot } from 'valtio'; +import { toast } from 'react-hot-toast'; +import { SiGitee, SiGithub } from 'react-icons/si'; +import Button from '@common/components/Button'; +import { userInfoStore, userEvent } from '@modules/auth/UserInfoStore'; +import client from '@graphql/client'; +import { UserinfoQuery, useUserUnbindMutation } from '@graphql/generated'; +import { setCallbackUrl } from '@common/utils/cookie'; + +const findBindInfo = ( + provider: string, + currentUser: DeepReadonly +) => { + if (currentUser?.loginBinds) { + return currentUser?.loginBinds.find((i) => { + return i.provider === provider; + }); + } + return null; +}; + +const UnBindBtn = ({ providerId }: { providerId: string }) => { + const { t } = useTranslation(); + const mutation = useUserUnbindMutation(client); + + return ( + + ); +}; + +const OAuthList = () => { + const { t } = useTranslation(); + const { currentUser } = useSnapshot(userInfoStore); + + const providers = [ + { + name: 'GitHub', + id: 'github', + desc: t('setting:profile.can_be_used_to_submit_project_after_binding'), + icon: , + }, + { + name: 'Gitee', + id: 'gitee', + desc: t('setting:profile.can_be_used_to_submit_project_after_binding'), + icon: , + }, + ]; + + return ( +
+
+ {t('setting:profile.connected_accounts')} +
+
+ {t( + 'setting:profile.connect_multiple_accounts_to_your_user_and_sign_in' + )} +
+
+ {providers.map((provider) => { + const bindInfo = findBindInfo(provider.id, currentUser); + return ( +
+
{provider.icon}
+
+
{provider.name}
+ {bindInfo ? ( +
+ {bindInfo.nickname} +
+ ) : ( +
{provider.desc}
+ )} +
+
+ {bindInfo ? ( + + ) : ( + + )} +
+
+ ); + })} +
+
+ ); +}; + +export default OAuthList; diff --git a/src/modules/settings/profile/ProfileForm.tsx b/src/modules/settings/profile/ProfileForm.tsx new file mode 100644 index 00000000..453c27b4 --- /dev/null +++ b/src/modules/settings/profile/ProfileForm.tsx @@ -0,0 +1,161 @@ +import React, { useEffect } from 'react'; +import Image from 'next/image'; +import { useSnapshot } from 'valtio'; +import { AiOutlineQuestionCircle } from 'react-icons/ai'; +import { useTranslation } from 'react-i18next'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import { userInfoStore } from '@modules/auth/UserInfoStore'; +import useProviderInfo from '@modules/auth/useProviderInfo'; +import Input from '@common/components/Input'; +import Button from '@common/components/Button'; +import Tooltip from '@common/components/Tooltip'; + +interface IFormInput { + name: string; + email: string; +} + +const ProfileForm = () => { + const { t } = useTranslation(); + const { providerUser } = useProviderInfo(); + const { currentUser } = useSnapshot(userInfoStore); + const name = currentUser?.name; + const email = currentUser?.email; + + const { + watch, + setValue, + register, + handleSubmit, + formState: { errors }, + } = useForm(); + const onSubmit: SubmitHandler = (data) => { + console.log(data); + }; + + const inputEmail = watch('email'); + + useEffect(() => { + if (name) setValue('name', name); + if (email) setValue('email', email); + }, [name, email, setValue]); + + return ( +
+

+ {t('setting:profile.userinfo')} +

+
+
+
+
+ {t('setting:profile.form.name')} +
+ + {errors['name']?.message && ( +

{errors['name'].message}

+ )} +
+ +
+
+
+ {t('setting:profile.form.email')} + {t('setting:profile.form.email_ques_icon')}} + placement="right" + > + + + + +
+ + {currentUser?.emailVerified ? null : ( +
+ {t('setting:profile.unverified_yet')} +
{}} + > + {t('setting:profile.resend_verification_email')} +
+
+ )} +
+
+ + + {currentUser?.emailVerified ? ( + inputEmail === email ? ( +
+ {t('setting:profile.verified')} +
+ ) : null + ) : null} +
+ {errors['email']?.message && ( +

{errors['email'].message}

+ )} +
+ + {t('common:btn.func_disabled')}} + placement="right" + > + + +
+ +
+
{t('setting:profile.avatar')}
+
+ {providerUser?.avatarUrl ? ( + avatar + ) : null} +
+
+
+
+ ); +}; + +export default ProfileForm; diff --git a/src/modules/settings/profile/index.tsx b/src/modules/settings/profile/index.tsx new file mode 100644 index 00000000..d1363315 --- /dev/null +++ b/src/modules/settings/profile/index.tsx @@ -0,0 +1,17 @@ +import React, { useEffect } from 'react'; +import Center from '@common/components/Layout/Center'; +import ProfileForm from './ProfileForm'; +import OAuthList from './OAuthList'; +import DeleteAccount from './DeleteAccount'; + +const ProfileSetting = () => { + return ( +
+ + + +
+ ); +}; + +export default ProfileSetting; diff --git a/src/modules/settings/subscribe/index.tsx b/src/modules/settings/subscribe/index.tsx new file mode 100644 index 00000000..b72259c1 --- /dev/null +++ b/src/modules/settings/subscribe/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { SiGitee, SiGithub } from 'react-icons/si'; +import Center from '@common/components/Layout/Center'; +import Button from '@common/components/Button'; + +const Subscribe = () => { + return ( +
+
+
My subscriptions
+
+ +
+
+ +
+
+
+ +
+
+
+
+
+ Updated on 2023-04-28 +
+
+
Unsubscribe
+
+
+ ); +}; + +export default Subscribe; diff --git a/src/modules/submitProject/Form/SelectRepoSource.tsx b/src/modules/submitProject/Form/SelectRepoSource.tsx index d51580a3..d22b58dc 100644 --- a/src/modules/submitProject/Form/SelectRepoSource.tsx +++ b/src/modules/submitProject/Form/SelectRepoSource.tsx @@ -4,8 +4,8 @@ import Image from 'next/image'; import { AiFillCaretDown, AiOutlinePlus } from 'react-icons/ai'; import { useClickAway, useSessionStorage } from 'react-use'; import { useQuery } from '@tanstack/react-query'; -import { useUserInfo } from '@modules/auth/UserInfoContext'; import { getOrganizations } from '@modules/submitProject/api'; +import useProviderInfo from '@modules/auth/useProviderInfo'; const SourceItem: React.FC<{ className?: string; @@ -32,7 +32,7 @@ const SourceItem: React.FC<{ interface Item { avatar_url: string; login: string; - user: boolean; + isUser: boolean; } const SelectRepoSource: React.FC< @@ -44,8 +44,7 @@ const SelectRepoSource: React.FC< > = ({ className, onChange, value }) => { const [open, setOpen] = useState(false); const ref = useRef(null); - - const { user } = useUserInfo(); + const { providerUser: user } = useProviderInfo(); const nickname = user?.nickname!; const account = user?.account!; const provider = user?.provider!; @@ -62,7 +61,11 @@ const SelectRepoSource: React.FC< const options: Item[] = React.useMemo(() => { const items = data?.data?.map((item) => { - return { login: item.login, avatar_url: item.avatar_url, user: false }; + return { + login: item.login, + avatar_url: item.avatar_url, + isUser: false, + }; }) || []; return [ @@ -70,7 +73,7 @@ const SelectRepoSource: React.FC< { login: nickname, avatar_url: avatarUrl!, - user: true, + isUser: true, }, ]; }, [data, nickname, avatarUrl]); diff --git a/src/modules/submitProject/FormCommunity/index.tsx b/src/modules/submitProject/FormCommunity/index.tsx index e1b5fe81..16acb271 100644 --- a/src/modules/submitProject/FormCommunity/index.tsx +++ b/src/modules/submitProject/FormCommunity/index.tsx @@ -9,14 +9,15 @@ import SwitchToSingleRepo from './SwitchToSingleRepo'; import SoftwareArtifactRepository from './SoftwareArtifactRepository'; import GovernanceRepository from './GovernanceRepository'; import { fillHttps, getRepoName } from '@common/utils'; -import { useUserInfo } from '@modules/auth/UserInfoContext'; +import useProviderInfo from '@modules/auth/useProviderInfo'; import Message from '@modules/submitProject/Misc/Message'; import { useTranslation } from 'react-i18next'; const FormCommunity = () => { const { t } = useTranslation(); - const { user } = useUserInfo(); + const { providerUser: user } = useProviderInfo(); const account = user!.account; + const provider = user!.provider; const [communityName, setCommunityName] = useState(''); const [sarUrls, setSarUrls] = useSessionStorage( @@ -48,7 +49,7 @@ const FormCommunity = () => { const handleSubmit = () => { const common = { - origin: user!.provider as string, + origin: provider as string, }; const projectName = communityName || options[0]; mutate({ diff --git a/src/modules/submitProject/FormSingleRepo/index.tsx b/src/modules/submitProject/FormSingleRepo/index.tsx index 91431662..a29d8795 100644 --- a/src/modules/submitProject/FormSingleRepo/index.tsx +++ b/src/modules/submitProject/FormSingleRepo/index.tsx @@ -8,7 +8,7 @@ import SelectLike from '@common/components/SelectLike'; import Input from '@common/components/Input'; import Button from '@common/components/Button'; import Message from '@modules/submitProject/Misc/Message'; -import { useUserInfo } from '@modules/auth/UserInfoContext'; +import useProviderInfo from '@modules/auth/useProviderInfo'; import { fillHttps } from '@common/utils'; import SwitchToCommunity from './SwitchToCommunity'; import RepoSelect from '../RepoSelect'; @@ -16,7 +16,7 @@ import { getUrlReg } from '../Misc'; const FormSingleRepo = () => { const { t } = useTranslation(); - const { user } = useUserInfo(); + const { providerUser: user } = useProviderInfo(); const provider = user?.provider || 'github'; const [formType, setFormType] = useState<'select' | 'input'>('input'); @@ -47,7 +47,7 @@ const FormSingleRepo = () => { const reportUrl = data?.createRepoTask?.reportUrl; const onSubmit: SubmitHandler<{ url?: string }> = (data) => { - const common = { origin: user?.provider as string }; + const common = { origin: provider }; const urls = [data.url, selectVal].map(fillHttps).filter(Boolean); mutate({ ...common, repoUrls: urls }); }; diff --git a/src/modules/submitProject/Misc/AddSelectPopover.tsx b/src/modules/submitProject/Misc/AddSelectPopover.tsx index c13ae796..67356c07 100644 --- a/src/modules/submitProject/Misc/AddSelectPopover.tsx +++ b/src/modules/submitProject/Misc/AddSelectPopover.tsx @@ -4,7 +4,7 @@ import { AiFillGithub, AiOutlineLink, AiOutlinePlus } from 'react-icons/ai'; import classnames from 'classnames'; import { SiGitee } from 'react-icons/si'; import { useTranslation } from 'react-i18next'; -import { useUserInfo } from '@modules/auth/UserInfoContext'; +import useProviderInfo from '@modules/auth/useProviderInfo'; export const getIcons = (type: string) => { switch (type) { @@ -25,7 +25,7 @@ const AddSelectPopover: React.FC<{ onClick: (e: React.MouseEvent) => void; }> = ({ className, onSelect, onClick, open, onClose }) => { const { t } = useTranslation(); - const { user } = useUserInfo(); + const { providerUser: user } = useProviderInfo(); const provider = user?.provider!; const ref = useRef(null); diff --git a/src/modules/submitProject/Misc/Auth.tsx b/src/modules/submitProject/Misc/Auth.tsx index 81b72a19..129c9ba6 100644 --- a/src/modules/submitProject/Misc/Auth.tsx +++ b/src/modules/submitProject/Misc/Auth.tsx @@ -1,22 +1,15 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import Image from 'next/image'; -import client from '@graphql/client'; -import { useRouter } from 'next/router'; import { useTranslation } from 'react-i18next'; -import { useUserInfo } from '@modules/auth/UserInfoContext'; -import { useSignOutMutation } from '@graphql/generated'; +import useProviderInfo from '@modules/auth/useProviderInfo'; const Auth: React.FC = () => { const { t } = useTranslation(); - const router = useRouter(); - const { user } = useUserInfo(); - const isLogin = Boolean(user); - const mutation = useSignOutMutation(client); + const { providerUser: user, loginBinds, toggle } = useProviderInfo(); + const hasLoggedIn = Boolean(user); + const bindLen = loginBinds?.length; - if (!isLogin) { - router.push('/auth/signin'); - return null; - } + if (!hasLoggedIn) return null; return ( <> @@ -48,22 +41,17 @@ const Auth: React.FC = () => {
-
- +
+ {bindLen && bindLen > 1 ? ( + + ) : null}
diff --git a/src/modules/submitProject/Misc/Banner/index.tsx b/src/modules/submitProject/Misc/Banner/index.tsx index 6d3b2908..c7ba0bf9 100644 --- a/src/modules/submitProject/Misc/Banner/index.tsx +++ b/src/modules/submitProject/Misc/Banner/index.tsx @@ -10,12 +10,12 @@ const Banner: React.FC<{ content: string }> = ({ content }) => { style.headerBgLine )} > - {/**/} +
{content}
diff --git a/src/modules/submitProject/Misc/FillItem.tsx b/src/modules/submitProject/Misc/FillItem.tsx index c45a79b1..0f1053da 100644 --- a/src/modules/submitProject/Misc/FillItem.tsx +++ b/src/modules/submitProject/Misc/FillItem.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { AiFillGithub, AiOutlineClose } from 'react-icons/ai'; import { SiGitee } from 'react-icons/si'; -import { useUserInfo } from '@modules/auth/UserInfoContext'; +import useProviderInfo from '@modules/auth/useProviderInfo'; export const getIcons = (type: string) => { switch (type) { @@ -18,7 +18,7 @@ const FillItem: React.FC<{ url: string; onDelete: (v: string) => void }> = ({ url, onDelete, }) => { - const { user } = useUserInfo(); + const { providerUser: user } = useProviderInfo(); const provider = user?.provider!; return ( diff --git a/src/modules/submitProject/Misc/InputUrlField.tsx b/src/modules/submitProject/Misc/InputUrlField.tsx index bbd3a5f1..4e46e812 100644 --- a/src/modules/submitProject/Misc/InputUrlField.tsx +++ b/src/modules/submitProject/Misc/InputUrlField.tsx @@ -9,7 +9,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; import Input from '@common/components/Input'; import { AiOutlineClose } from 'react-icons/ai'; -import { useUserInfo } from '@modules/auth/UserInfoContext'; +import useProviderInfo from '@modules/auth/useProviderInfo'; import { getUrlReg } from '../Misc'; interface Props { @@ -26,7 +26,7 @@ const InputUrlField = forwardRef( ({ onClose, onPressEnter }, ref) => { const { t } = useTranslation(); const inputRef = useRef(null); - const { user } = useUserInfo(); + const { providerUser: user } = useProviderInfo(); const provider = user?.provider!; const [value, setValue] = useState(''); diff --git a/src/modules/submitProject/RepoSelect/index.tsx b/src/modules/submitProject/RepoSelect/index.tsx index bcad8f0b..ea1881be 100644 --- a/src/modules/submitProject/RepoSelect/index.tsx +++ b/src/modules/submitProject/RepoSelect/index.tsx @@ -10,7 +10,7 @@ import { useDebounce } from 'ahooks'; import Input from '@common/components/Input'; import { CgSpinner } from 'react-icons/cg'; import SelectRepoSource from '@modules/submitProject/Form/SelectRepoSource'; -import { useUserInfo } from '@modules/auth/UserInfoContext'; +import useProviderInfo from '@modules/auth/useProviderInfo'; import RepoItem from './RepoItem'; import Loading from './Loading'; import { useTranslation } from 'react-i18next'; @@ -20,7 +20,7 @@ const RepoSelect: React.FC<{ onConfirm: (val: string) => void }> = ({ onConfirm, }) => { const { t } = useTranslation(); - const { user } = useUserInfo(); + const { providerUser: user } = useProviderInfo(); const nickname = user?.nickname!; const account = user?.account!; const provider = user?.provider!; @@ -30,7 +30,7 @@ const RepoSelect: React.FC<{ onConfirm: (val: string) => void }> = ({ const [org, setOrg] = useState({ login: nickname!, avatar_url: user?.avatarUrl!, - user: true, + isUser: true, }); const [repoList, setRepoList] = useState([]); @@ -40,7 +40,7 @@ const RepoSelect: React.FC<{ onConfirm: (val: string) => void }> = ({ const { isLoading, isFetching, isError, error } = useQuery( ['getRepos', account, page, { org }], () => { - if (org.user) { + if (org.isUser) { return getRepos(provider)({ username: account, page }); } return getOrgRepos(provider)({ diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 1f44cb00..202dc191 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -3,9 +3,11 @@ import Head from 'next/head'; import dynamic from 'next/dynamic'; import App, { AppContext, AppProps } from 'next/app'; import { appWithTranslation } from 'next-i18next'; +import { Toaster } from 'react-hot-toast'; import i18nextConfig from 'next-i18next.config.js'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import UserInfoFetcher from '@modules/auth/UserInfoFetcher'; import { useAppGA, GAScripts } from '@common/lib/ga'; import { browserLanguageDetectorAndReload } from '@common/utils/getLocale'; @@ -69,6 +71,18 @@ function MyApp({ color="#3A5BEF" options={{ showSpinner: false }} /> + + diff --git a/src/pages/auth/email/verify/failed.tsx b/src/pages/auth/email/verify/failed.tsx new file mode 100644 index 00000000..f3a2c54e --- /dev/null +++ b/src/pages/auth/email/verify/failed.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const Failed = () => { + return
; +}; + +export default Failed; diff --git a/src/pages/auth/email/verify/success.tsx b/src/pages/auth/email/verify/success.tsx new file mode 100644 index 00000000..8d84dd3f --- /dev/null +++ b/src/pages/auth/email/verify/success.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const Success = () => { + return
; +}; + +export default Success; diff --git a/src/pages/auth/login/index.tsx b/src/pages/auth/login/index.tsx deleted file mode 100644 index 9e201889..00000000 --- a/src/pages/auth/login/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import Copyright from '@modules/auth/components/Copyright'; -import LogoHeader from '@modules/auth/components/LogoHeader'; -import Image from 'next/image'; -import { useTranslation } from 'next-i18next'; -import getLocalesFile from '@common/utils/getLocalesFile'; -import { GetServerSidePropsContext } from 'next'; - -export async function getServerSideProps(context: GetServerSidePropsContext) { - const { req } = context; - return { - props: { - ...(await getLocalesFile(req.cookies, ['submit_project'])), - }, - }; -} -const Login: React.FC = () => { - const { t } = useTranslation(); - - return ( -
- -
-

- {t('submit_project:welcome_to')} -

-
- {'github'} - - {t('submit_project:continue_with_github')} - -
-
- {'gitee'} - - {t('submit_project:continue_with_gitee')} - -
-
- {t('submit_project:by_creating_an_account')} - - {t('submit_project:terms_of_use')} - - {t('submit_project:as_well_as')} - - {t('submit_project:non_active_account_processing_specification')} - -
-
- -
- ); -}; - -export default Login; diff --git a/src/pages/auth/signin.tsx b/src/pages/auth/signin.tsx index 6a98ff5e..a6d620e4 100644 --- a/src/pages/auth/signin.tsx +++ b/src/pages/auth/signin.tsx @@ -1,10 +1,12 @@ import React from 'react'; -import { GetServerSidePropsContext } from 'next'; +import { useRouter } from 'next/router'; import { useTranslation } from 'next-i18next'; -import Header from '@common/components/Header'; -import Banner from '@modules/submitProject/Misc/Banner'; -import { UserInfoProvider } from '@modules/auth/UserInfoContext'; -import LoginPage from '@modules/auth/LoginPage'; +import Link from 'next/link'; +import { GetServerSidePropsContext } from 'next'; +import { FiAlertCircle } from 'react-icons/fi'; +import Copyright from '@modules/auth/components/Copyright'; +import LogoHeader from '@modules/auth/components/LogoHeader'; +import LoginItems from '@modules/auth/LoginItems'; import getLocalesFile from '@common/utils/getLocalesFile'; export async function getServerSideProps(context: GetServerSidePropsContext) { @@ -18,12 +20,45 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { const SignIn: React.FC = () => { const { t } = useTranslation(); + const router = useRouter(); + const error = router.query.error; + return ( - -
- - - +
+ + + {error && ( +

+ {error} +

+ )} + +
+

+ {t('submit_project:welcome_to')} +

+ + + +
+ {t('submit_project:by_creating_an_account')} + + + {t('submit_project:terms_of_use')} + + + + {t('submit_project:as_well_as')} + + + + {t('submit_project:privacy_policy')} + + +
+
+ +
); }; diff --git a/src/pages/settings/profile.tsx b/src/pages/settings/profile.tsx new file mode 100644 index 00000000..14ed74aa --- /dev/null +++ b/src/pages/settings/profile.tsx @@ -0,0 +1,49 @@ +import React, { useEffect } from 'react'; +import { GetServerSideProps } from 'next'; +import { useTranslation } from 'react-i18next'; +import { useRouter } from 'next/router'; +import { toast } from 'react-hot-toast'; +import Header from '@common/components/Header'; +import Banner from '@modules/submitProject/Misc/Banner'; +import AuthRequire from '@modules/auth/AuthRequire'; +import ProfileSetting from '@modules/settings/profile'; +import getLocalesFile from '@common/utils/getLocalesFile'; + +export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + return { + props: { + ...(await getLocalesFile(req.cookies, ['setting'])), + }, + }; +}; + +const Settings = () => { + const { query } = useRouter(); + const { t } = useTranslation(); + + useEffect(() => { + let t: number; + if (query.error) { + t = window.setTimeout(() => { + toast.error((t) => <>{query.error}, { + position: 'top-center', + }); + }, 400); + } + return () => { + t && clearTimeout(t); + }; + }, [query.error]); + + return ( + <> +
+ + + + + + ); +}; + +export default Settings; diff --git a/src/pages/settings/subscribe.tsx b/src/pages/settings/subscribe.tsx new file mode 100644 index 00000000..83a7ec1e --- /dev/null +++ b/src/pages/settings/subscribe.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { GetServerSideProps } from 'next'; +import { useTranslation } from 'react-i18next'; +import AuthRequire from '@modules/auth/AuthRequire'; +import Header from '@common/components/Header'; +import Banner from '@modules/submitProject/Misc/Banner'; +import Subscribe from '@modules/settings/subscribe'; +import getLocalesFile from '@common/utils/getLocalesFile'; + +export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + return { + props: { + ...(await getLocalesFile(req.cookies, [])), + }, + }; +}; + +const SubscribePage = () => { + const { t } = useTranslation(); + return ( + <> +
+ + + + + + ); +}; + +export default SubscribePage; diff --git a/src/pages/submit-your-project/community.tsx b/src/pages/submit-your-project/community.tsx index bade2600..0fb2804f 100644 --- a/src/pages/submit-your-project/community.tsx +++ b/src/pages/submit-your-project/community.tsx @@ -3,8 +3,8 @@ import Header from '@common/components/Header'; import Banner from '@modules/submitProject/Misc/Banner'; import SubmitProject from '@modules/submitProject'; import FormCommunity from '@modules/submitProject/FormCommunity'; +import AuthRequire from '@modules/auth/AuthRequire'; import { GetServerSidePropsContext } from 'next'; -import { UserInfoProvider } from '@modules/auth/UserInfoContext'; import getLocalesFile from '@common/utils/getLocalesFile'; import { useTranslation } from 'react-i18next'; @@ -20,13 +20,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { const SubmitYourProject: React.FC = () => { const { t } = useTranslation(); return ( - + <>
- - - - + + + + + + ); }; diff --git a/src/pages/submit-your-project/index.tsx b/src/pages/submit-your-project/index.tsx index efa24201..dfcc5306 100644 --- a/src/pages/submit-your-project/index.tsx +++ b/src/pages/submit-your-project/index.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { GetServerSidePropsContext } from 'next'; +import AuthRequire from '@modules/auth/AuthRequire'; import Header from '@common/components/Header'; import Banner from '@modules/submitProject/Misc/Banner'; import SubmitProject from '@modules/submitProject'; import FormSingleRepo from '@modules/submitProject/FormSingleRepo'; -import { UserInfoProvider } from '@modules/auth/UserInfoContext'; import getLocalesFile from '@common/utils/getLocalesFile'; import { useTranslation } from 'react-i18next'; @@ -19,14 +19,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { const SubmitYourProject = () => { const { t } = useTranslation(); + return ( - + <>
- - - - + + + + + + ); }; diff --git a/yarn.lock b/yarn.lock index 85b78fdd..bc3eb926 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6670,6 +6670,11 @@ create-require@^1.1.0: resolved "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cropperjs@^1.5.13: + version "1.5.13" + resolved "https://registry.npmmirror.com/cropperjs/-/cropperjs-1.5.13.tgz#eb1682f01d17c70ed5244317091d745c9a249ef8" + integrity sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA== + cross-env@^7.0.3: version "7.0.3" resolved "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz" @@ -8695,6 +8700,11 @@ globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +goober@^2.1.10: + version "2.1.13" + resolved "https://registry.npmmirror.com/goober/-/goober-2.1.13.tgz#e3c06d5578486212a76c9eba860cbc3232ff6d7c" + integrity sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ== + gopd@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -12278,6 +12288,13 @@ react-colorful@^5.1.2: resolved "https://registry.npmmirror.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== +react-cropper@^2.3.3: + version "2.3.3" + resolved "https://registry.npmmirror.com/react-cropper/-/react-cropper-2.3.3.tgz#aced0d045d9fd56168ba4d17287a40c287754dee" + integrity sha512-zghiEYkUb41kqtu+2jpX2Ntigf+Jj1dF9ew4lAobPzI2adaPE31z0p+5TcWngK6TvmWQUwK3lj4G+NDh1PDQ1w== + dependencies: + cropperjs "^1.5.13" + react-datepicker@^4.11.0: version "4.11.0" resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.11.0.tgz#40e73b4729a284ed206fdb322b8e84eb566e11a3" @@ -12345,6 +12362,13 @@ react-hook-form@^7.36.0: resolved "https://registry.npmmirror.com/react-hook-form/-/react-hook-form-7.38.0.tgz" integrity sha512-gxWW1kMeru9xR1GoR+Iw4hA+JBOM3SHfr4DWCUKY0xc7Vv1MLsF109oHtBeWl9shcyPFx67KHru44DheN0XY5A== +react-hot-toast@^2.4.1: + version "2.4.1" + resolved "https://registry.npmmirror.com/react-hot-toast/-/react-hot-toast-2.4.1.tgz#df04295eda8a7b12c4f968e54a61c8d36f4c0994" + integrity sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ== + dependencies: + goober "^2.1.10" + react-hotkeys-hook@^3.4.7: version "3.4.7" resolved "https://registry.npmmirror.com/react-hotkeys-hook/-/react-hotkeys-hook-3.4.7.tgz"