activateForm.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. 'use client'
  2. import { useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import useSWR from 'swr'
  5. import { useSearchParams } from 'next/navigation'
  6. import cn from 'classnames'
  7. import Link from 'next/link'
  8. import { CheckCircleIcon } from '@heroicons/react/24/solid'
  9. import style from './style.module.css'
  10. import Button from '@/app/components/base/button'
  11. import { SimpleSelect } from '@/app/components/base/select'
  12. import { timezones } from '@/utils/timezone'
  13. import { languageMaps, languages } from '@/utils/language'
  14. import { activateMember, invitationCheck } from '@/service/common'
  15. import Toast from '@/app/components/base/toast'
  16. import Loading from '@/app/components/base/loading'
  17. const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
  18. const ActivateForm = () => {
  19. const { t } = useTranslation()
  20. const searchParams = useSearchParams()
  21. const workspaceID = searchParams.get('workspace_id')
  22. const email = searchParams.get('email')
  23. const token = searchParams.get('token')
  24. const checkParams = {
  25. url: '/activate/check',
  26. params: {
  27. workspace_id: workspaceID,
  28. email,
  29. token,
  30. },
  31. }
  32. const { data: checkRes, mutate: recheck } = useSWR(checkParams, invitationCheck, {
  33. revalidateOnFocus: false,
  34. })
  35. const [name, setName] = useState('')
  36. const [password, setPassword] = useState('')
  37. const [timezone, setTimezone] = useState('Asia/Shanghai')
  38. const [language, setLanguage] = useState('en-US')
  39. const [showSuccess, setShowSuccess] = useState(false)
  40. const showErrorMessage = (message: string) => {
  41. Toast.notify({
  42. type: 'error',
  43. message,
  44. })
  45. }
  46. const valid = () => {
  47. if (!name.trim()) {
  48. showErrorMessage(t('login.error.nameEmpty'))
  49. return false
  50. }
  51. if (!password.trim()) {
  52. showErrorMessage(t('login.error.passwordEmpty'))
  53. return false
  54. }
  55. if (!validPassword.test(password))
  56. showErrorMessage(t('login.error.passwordInvalid'))
  57. return true
  58. }
  59. const handleActivate = async () => {
  60. if (!valid())
  61. return
  62. try {
  63. await activateMember({
  64. url: '/activate',
  65. body: {
  66. workspace_id: workspaceID,
  67. email,
  68. token,
  69. name,
  70. password,
  71. interface_language: language,
  72. timezone,
  73. },
  74. })
  75. setShowSuccess(true)
  76. }
  77. catch {
  78. recheck()
  79. }
  80. }
  81. return (
  82. <div className={
  83. cn(
  84. 'flex flex-col items-center w-full grow items-center justify-center',
  85. 'px-6',
  86. 'md:px-[108px]',
  87. )
  88. }>
  89. {!checkRes && <Loading/>}
  90. {checkRes && !checkRes.is_valid && (
  91. <div className="flex flex-col md:w-[400px]">
  92. <div className="w-full mx-auto">
  93. <div className="mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold">🤷‍♂️</div>
  94. <h2 className="text-[32px] font-bold text-gray-900">{t('login.invalid')}</h2>
  95. </div>
  96. <div className="w-full mx-auto mt-6">
  97. <Button type='primary' className='w-full !fone-medium !text-sm'>
  98. <a href="https://dify.ai">{t('login.explore')}</a>
  99. </Button>
  100. </div>
  101. </div>
  102. )}
  103. {checkRes && checkRes.is_valid && !showSuccess && (
  104. <div className='flex flex-col md:w-[400px]'>
  105. <div className="w-full mx-auto">
  106. <div className={`mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold ${style.logo}`}>
  107. </div>
  108. <h2 className="text-[32px] font-bold text-gray-900">
  109. {`${t('login.join')} ${checkRes.workspace_name}`}
  110. </h2>
  111. <p className='mt-1 text-sm text-gray-600 '>
  112. {`${t('login.joinTipStart')} ${checkRes.workspace_name} ${t('login.joinTipEnd')}`}
  113. </p>
  114. </div>
  115. <div className="w-full mx-auto mt-6">
  116. <div className="bg-white">
  117. {/* username */}
  118. <div className='mb-5'>
  119. <label htmlFor="name" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900">
  120. {t('login.name')}
  121. </label>
  122. <div className="mt-1 relative rounded-md shadow-sm">
  123. <input
  124. id="name"
  125. type="text"
  126. value={name}
  127. onChange={e => setName(e.target.value)}
  128. placeholder={t('login.namePlaceholder') || ''}
  129. className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'}
  130. />
  131. </div>
  132. </div>
  133. {/* password */}
  134. <div className='mb-5'>
  135. <label htmlFor="password" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900">
  136. {t('login.password')}
  137. </label>
  138. <div className="mt-1 relative rounded-md shadow-sm">
  139. <input
  140. id="password"
  141. type='password'
  142. value={password}
  143. onChange={e => setPassword(e.target.value)}
  144. placeholder={t('login.passwordPlaceholder') || ''}
  145. className={'appearance-none block w-full rounded-lg pl-[14px] px-3 py-2 border border-gray-200 hover:border-gray-300 hover:shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 placeholder-gray-400 caret-primary-600 sm:text-sm pr-10'}
  146. />
  147. </div>
  148. <div className='mt-1 text-xs text-gray-500'>{t('login.error.passwordInvalid')}</div>
  149. </div>
  150. {/* language */}
  151. <div className='mb-5'>
  152. <label htmlFor="name" className="my-2 flex items-center justify-between text-sm font-medium text-gray-900">
  153. {t('login.interfaceLanguage')}
  154. </label>
  155. <div className="relative mt-1 rounded-md shadow-sm">
  156. <SimpleSelect
  157. defaultValue={languageMaps.en}
  158. items={languages}
  159. onSelect={(item) => {
  160. setLanguage(item.value as string)
  161. }}
  162. />
  163. </div>
  164. </div>
  165. {/* timezone */}
  166. <div className='mb-4'>
  167. <label htmlFor="timezone" className="block text-sm font-medium text-gray-700">
  168. {t('login.timezone')}
  169. </label>
  170. <div className="relative mt-1 rounded-md shadow-sm">
  171. <SimpleSelect
  172. defaultValue={timezone}
  173. items={timezones}
  174. onSelect={(item) => {
  175. setTimezone(item.value as string)
  176. }}
  177. />
  178. </div>
  179. </div>
  180. <div>
  181. <Button
  182. type='primary'
  183. className='w-full !fone-medium !text-sm'
  184. onClick={handleActivate}
  185. >
  186. {`${t('login.join')} ${checkRes.workspace_name}`}
  187. </Button>
  188. </div>
  189. <div className="block w-hull mt-2 text-xs text-gray-600">
  190. {t('login.license.tip')}
  191. &nbsp;
  192. <Link
  193. className='text-primary-600'
  194. target={'_blank'}
  195. href='https://docs.dify.ai/community/open-source'
  196. >{t('login.license.link')}</Link>
  197. </div>
  198. </div>
  199. </div>
  200. </div>
  201. )}
  202. {checkRes && checkRes.is_valid && showSuccess && (
  203. <div className="flex flex-col md:w-[400px]">
  204. <div className="w-full mx-auto">
  205. <div className="mb-3 flex justify-center items-center w-20 h-20 p-5 rounded-[20px] border border-gray-100 shadow-lg text-[40px] font-bold">
  206. <CheckCircleIcon className='w-10 h-10 text-[#039855]' />
  207. </div>
  208. <h2 className="text-[32px] font-bold text-gray-900">
  209. {`${t('login.activatedTipStart')} ${checkRes.workspace_name} ${t('login.activatedTipEnd')}`}
  210. </h2>
  211. </div>
  212. <div className="w-full mx-auto mt-6">
  213. <Button type='primary' className='w-full !fone-medium !text-sm'>
  214. <a href="/signin">{t('login.activated')}</a>
  215. </Button>
  216. </div>
  217. </div>
  218. )}
  219. </div>
  220. )
  221. }
  222. export default ActivateForm