index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. 'use client'
  2. import { useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import {
  5. RiGraduationCapFill,
  6. } from '@remixicon/react'
  7. import { useContext } from 'use-context-selector'
  8. import DeleteAccount from '../delete-account'
  9. import s from './index.module.css'
  10. import AvatarWithEdit from './AvatarWithEdit'
  11. import Collapse from '@/app/components/header/account-setting/collapse'
  12. import type { IItem } from '@/app/components/header/account-setting/collapse'
  13. import Modal from '@/app/components/base/modal'
  14. import Button from '@/app/components/base/button'
  15. import { updateUserProfile } from '@/service/common'
  16. import { useAppContext } from '@/context/app-context'
  17. import { useProviderContext } from '@/context/provider-context'
  18. import { ToastContext } from '@/app/components/base/toast'
  19. import AppIcon from '@/app/components/base/app-icon'
  20. import { IS_CE_EDITION } from '@/config'
  21. import Input from '@/app/components/base/input'
  22. import PremiumBadge from '@/app/components/base/premium-badge'
  23. const titleClassName = `
  24. system-sm-semibold text-text-secondary
  25. `
  26. const descriptionClassName = `
  27. mt-1 body-xs-regular text-text-tertiary
  28. `
  29. const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
  30. export default function AccountPage() {
  31. const { t } = useTranslation()
  32. const { systemFeatures } = useAppContext()
  33. const { mutateUserProfile, userProfile, apps } = useAppContext()
  34. const { isEducationAccount } = useProviderContext()
  35. const { notify } = useContext(ToastContext)
  36. const [editNameModalVisible, setEditNameModalVisible] = useState(false)
  37. const [editName, setEditName] = useState('')
  38. const [editing, setEditing] = useState(false)
  39. const [editPasswordModalVisible, setEditPasswordModalVisible] = useState(false)
  40. const [currentPassword, setCurrentPassword] = useState('')
  41. const [password, setPassword] = useState('')
  42. const [confirmPassword, setConfirmPassword] = useState('')
  43. const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false)
  44. const [showCurrentPassword, setShowCurrentPassword] = useState(false)
  45. const [showPassword, setShowPassword] = useState(false)
  46. const [showConfirmPassword, setShowConfirmPassword] = useState(false)
  47. const handleEditName = () => {
  48. setEditNameModalVisible(true)
  49. setEditName(userProfile.name)
  50. }
  51. const handleSaveName = async () => {
  52. try {
  53. setEditing(true)
  54. await updateUserProfile({ url: 'account/name', body: { name: editName } })
  55. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  56. mutateUserProfile()
  57. setEditNameModalVisible(false)
  58. setEditing(false)
  59. }
  60. catch (e) {
  61. notify({ type: 'error', message: (e as Error).message })
  62. setEditNameModalVisible(false)
  63. setEditing(false)
  64. }
  65. }
  66. const showErrorMessage = (message: string) => {
  67. notify({
  68. type: 'error',
  69. message,
  70. })
  71. }
  72. const valid = () => {
  73. if (!password.trim()) {
  74. showErrorMessage(t('login.error.passwordEmpty'))
  75. return false
  76. }
  77. if (!validPassword.test(password)) {
  78. showErrorMessage(t('login.error.passwordInvalid'))
  79. return false
  80. }
  81. if (password !== confirmPassword) {
  82. showErrorMessage(t('common.account.notEqual'))
  83. return false
  84. }
  85. return true
  86. }
  87. const resetPasswordForm = () => {
  88. setCurrentPassword('')
  89. setPassword('')
  90. setConfirmPassword('')
  91. }
  92. const handleSavePassword = async () => {
  93. if (!valid())
  94. return
  95. try {
  96. setEditing(true)
  97. await updateUserProfile({
  98. url: 'account/password',
  99. body: {
  100. password: currentPassword,
  101. new_password: password,
  102. repeat_new_password: confirmPassword,
  103. },
  104. })
  105. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  106. mutateUserProfile()
  107. setEditPasswordModalVisible(false)
  108. resetPasswordForm()
  109. setEditing(false)
  110. }
  111. catch (e) {
  112. notify({ type: 'error', message: (e as Error).message })
  113. setEditPasswordModalVisible(false)
  114. setEditing(false)
  115. }
  116. }
  117. const renderAppItem = (item: IItem) => {
  118. return (
  119. <div className='flex px-3 py-1'>
  120. <div className='mr-3'>
  121. <AppIcon size='tiny' />
  122. </div>
  123. <div className='system-sm-medium mt-[3px] text-text-secondary'>{item.name}</div>
  124. </div>
  125. )
  126. }
  127. return (
  128. <>
  129. <div className='pb-3 pt-2'>
  130. <h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
  131. </div>
  132. <div className='mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6'>
  133. <AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={ mutateUserProfile } size={64} />
  134. <div className='ml-4'>
  135. <p className='system-xl-semibold text-text-primary'>
  136. {userProfile.name}
  137. {isEducationAccount && (
  138. <PremiumBadge size='s' color='blue' className='ml-1 !px-2'>
  139. <RiGraduationCapFill className='w-3 h-3 mr-1' />
  140. <span className='system-2xs-medium'>EDU</span>
  141. </PremiumBadge>
  142. )}
  143. </p>
  144. <p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>
  145. </div>
  146. </div>
  147. <div className='mb-8'>
  148. <div className={titleClassName}>{t('common.account.name')}</div>
  149. <div className='mt-2 flex w-full items-center justify-between gap-2'>
  150. <div className='system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled '>
  151. <span className='pl-1'>{userProfile.name}</span>
  152. </div>
  153. <div className='system-sm-medium cursor-pointer rounded-lg bg-components-button-tertiary-bg px-3 py-2 text-components-button-tertiary-text' onClick={handleEditName}>
  154. {t('common.operation.edit')}
  155. </div>
  156. </div>
  157. </div>
  158. <div className='mb-8'>
  159. <div className={titleClassName}>{t('common.account.email')}</div>
  160. <div className='mt-2 flex w-full items-center justify-between gap-2'>
  161. <div className='system-sm-regular flex-1 rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled '>
  162. <span className='pl-1'>{userProfile.email}</span>
  163. </div>
  164. </div>
  165. </div>
  166. {
  167. systemFeatures.enable_email_password_login && (
  168. <div className='mb-8 flex justify-between gap-2'>
  169. <div>
  170. <div className='system-sm-semibold mb-1 text-text-secondary'>{t('common.account.password')}</div>
  171. <div className='body-xs-regular mb-2 text-text-tertiary'>{t('common.account.passwordTip')}</div>
  172. </div>
  173. <Button onClick={() => setEditPasswordModalVisible(true)}>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</Button>
  174. </div>
  175. )
  176. }
  177. <div className='mb-6 border-[1px] border-divider-subtle' />
  178. <div className='mb-8'>
  179. <div className={titleClassName}>{t('common.account.langGeniusAccount')}</div>
  180. <div className={descriptionClassName}>{t('common.account.langGeniusAccountTip')}</div>
  181. {!!apps.length && (
  182. <Collapse
  183. title={`${t('common.account.showAppLength', { length: apps.length })}`}
  184. items={apps.map(app => ({ key: app.id, name: app.name }))}
  185. renderItem={renderAppItem}
  186. wrapperClassName='mt-2'
  187. />
  188. )}
  189. {!IS_CE_EDITION && <Button className='mt-2 text-components-button-destructive-secondary-text' onClick={() => setShowDeleteAccountModal(true)}>{t('common.account.delete')}</Button>}
  190. </div>
  191. {
  192. editNameModalVisible && (
  193. <Modal
  194. isShow
  195. onClose={() => setEditNameModalVisible(false)}
  196. className={s.modal}
  197. >
  198. <div className='title-2xl-semi-bold mb-6 text-text-primary'>{t('common.account.editName')}</div>
  199. <div className={titleClassName}>{t('common.account.name')}</div>
  200. <Input className='mt-2'
  201. value={editName}
  202. onChange={e => setEditName(e.target.value)}
  203. />
  204. <div className='mt-10 flex justify-end'>
  205. <Button className='mr-2' onClick={() => setEditNameModalVisible(false)}>{t('common.operation.cancel')}</Button>
  206. <Button
  207. disabled={editing || !editName}
  208. variant='primary'
  209. onClick={handleSaveName}
  210. >
  211. {t('common.operation.save')}
  212. </Button>
  213. </div>
  214. </Modal>
  215. )
  216. }
  217. {
  218. editPasswordModalVisible && (
  219. <Modal
  220. isShow
  221. onClose={() => {
  222. setEditPasswordModalVisible(false)
  223. resetPasswordForm()
  224. }}
  225. className={s.modal}
  226. >
  227. <div className='title-2xl-semi-bold mb-6 text-text-primary'>{userProfile.is_password_set ? t('common.account.resetPassword') : t('common.account.setPassword')}</div>
  228. {userProfile.is_password_set && (
  229. <>
  230. <div className={titleClassName}>{t('common.account.currentPassword')}</div>
  231. <div className='relative mt-2'>
  232. <Input
  233. type={showCurrentPassword ? 'text' : 'password'}
  234. value={currentPassword}
  235. onChange={e => setCurrentPassword(e.target.value)}
  236. />
  237. <div className="absolute inset-y-0 right-0 flex items-center">
  238. <Button
  239. type="button"
  240. variant='ghost'
  241. onClick={() => setShowCurrentPassword(!showCurrentPassword)}
  242. >
  243. {showCurrentPassword ? '👀' : '😝'}
  244. </Button>
  245. </div>
  246. </div>
  247. </>
  248. )}
  249. <div className='system-sm-semibold mt-8 text-text-secondary'>
  250. {userProfile.is_password_set ? t('common.account.newPassword') : t('common.account.password')}
  251. </div>
  252. <div className='relative mt-2'>
  253. <Input
  254. type={showPassword ? 'text' : 'password'}
  255. value={password}
  256. onChange={e => setPassword(e.target.value)}
  257. />
  258. <div className="absolute inset-y-0 right-0 flex items-center">
  259. <Button
  260. type="button"
  261. variant='ghost'
  262. onClick={() => setShowPassword(!showPassword)}
  263. >
  264. {showPassword ? '👀' : '😝'}
  265. </Button>
  266. </div>
  267. </div>
  268. <div className='system-sm-semibold mt-8 text-text-secondary'>{t('common.account.confirmPassword')}</div>
  269. <div className='relative mt-2'>
  270. <Input
  271. type={showConfirmPassword ? 'text' : 'password'}
  272. value={confirmPassword}
  273. onChange={e => setConfirmPassword(e.target.value)}
  274. />
  275. <div className="absolute inset-y-0 right-0 flex items-center">
  276. <Button
  277. type="button"
  278. variant='ghost'
  279. onClick={() => setShowConfirmPassword(!showConfirmPassword)}
  280. >
  281. {showConfirmPassword ? '👀' : '😝'}
  282. </Button>
  283. </div>
  284. </div>
  285. <div className='mt-10 flex justify-end'>
  286. <Button className='mr-2' onClick={() => {
  287. setEditPasswordModalVisible(false)
  288. resetPasswordForm()
  289. }}>{t('common.operation.cancel')}</Button>
  290. <Button
  291. disabled={editing}
  292. variant='primary'
  293. onClick={handleSavePassword}
  294. >
  295. {userProfile.is_password_set ? t('common.operation.reset') : t('common.operation.save')}
  296. </Button>
  297. </div>
  298. </Modal>
  299. )
  300. }
  301. {
  302. showDeleteAccountModal && (
  303. <DeleteAccount
  304. onCancel={() => setShowDeleteAccountModal(false)}
  305. onConfirm={() => setShowDeleteAccountModal(false)}
  306. />
  307. )
  308. }
  309. </>
  310. )
  311. }