index.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useEffect, useState } from 'react'
  4. import { ChevronRightIcon } from '@heroicons/react/20/solid'
  5. import Link from 'next/link'
  6. import { Trans, useTranslation } from 'react-i18next'
  7. import s from './style.module.css'
  8. import Modal from '@/app/components/base/modal'
  9. import Button from '@/app/components/base/button'
  10. import AppIcon from '@/app/components/base/app-icon'
  11. import { SimpleSelect } from '@/app/components/base/select'
  12. import type { AppDetailResponse } from '@/models/app'
  13. import type { Language } from '@/types/app'
  14. import EmojiPicker from '@/app/components/base/emoji-picker'
  15. export type ISettingsModalProps = {
  16. appInfo: AppDetailResponse
  17. isShow: boolean
  18. defaultValue?: string
  19. onClose: () => void
  20. onSave?: (params: ConfigParams) => Promise<void>
  21. }
  22. export type ConfigParams = {
  23. title: string
  24. description: string
  25. default_language: string
  26. prompt_public: boolean
  27. copyright: string
  28. privacy_policy: string
  29. icon: string
  30. icon_background: string
  31. }
  32. const LANGUAGE_MAP: Record<Language, string> = {
  33. 'en-US': 'English(United States)',
  34. 'zh-Hans': '简体中文',
  35. }
  36. const prefixSettings = 'appOverview.overview.appInfo.settings'
  37. const SettingsModal: FC<ISettingsModalProps> = ({
  38. appInfo,
  39. isShow = false,
  40. onClose,
  41. onSave,
  42. }) => {
  43. const [isShowMore, setIsShowMore] = useState(false)
  44. const { icon, icon_background } = appInfo
  45. const { title, description, copyright, privacy_policy, default_language } = appInfo.site
  46. const [inputInfo, setInputInfo] = useState({ title, desc: description, copyright, privacyPolicy: privacy_policy })
  47. const [language, setLanguage] = useState(default_language)
  48. const [saveLoading, setSaveLoading] = useState(false)
  49. const { t } = useTranslation()
  50. // Emoji Picker
  51. const [showEmojiPicker, setShowEmojiPicker] = useState(false)
  52. const [emoji, setEmoji] = useState({ icon, icon_background })
  53. useEffect(() => {
  54. setInputInfo({ title, desc: description, copyright, privacyPolicy: privacy_policy })
  55. setLanguage(default_language)
  56. setEmoji({ icon, icon_background })
  57. }, [appInfo])
  58. const onHide = () => {
  59. onClose()
  60. setTimeout(() => {
  61. setIsShowMore(false)
  62. }, 200)
  63. }
  64. const onClickSave = async () => {
  65. setSaveLoading(true)
  66. const params = {
  67. title: inputInfo.title,
  68. description: inputInfo.desc,
  69. default_language: language,
  70. prompt_public: false,
  71. copyright: inputInfo.copyright,
  72. privacy_policy: inputInfo.privacyPolicy,
  73. icon: emoji.icon,
  74. icon_background: emoji.icon_background,
  75. }
  76. await onSave?.(params)
  77. setSaveLoading(false)
  78. onHide()
  79. }
  80. const onChange = (field: string) => {
  81. return (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
  82. setInputInfo(item => ({ ...item, [field]: e.target.value }))
  83. }
  84. }
  85. return (
  86. <>
  87. <Modal
  88. title={t(`${prefixSettings}.title`)}
  89. isShow={isShow}
  90. onClose={onHide}
  91. className={`${s.settingsModal}`}
  92. >
  93. <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.webName`)}</div>
  94. <div className='flex mt-2'>
  95. <AppIcon size='large'
  96. onClick={() => { setShowEmojiPicker(true) }}
  97. className='cursor-pointer !mr-3 self-center'
  98. icon={emoji.icon}
  99. background={emoji.icon_background}
  100. />
  101. <input className={`flex-grow rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
  102. value={inputInfo.title}
  103. onChange={onChange('title')}
  104. placeholder={t('app.appNamePlaceholder') || ''}
  105. />
  106. </div>
  107. <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.webDesc`)}</div>
  108. <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.webDescTip`)}</p>
  109. <textarea
  110. rows={3}
  111. className={`mt-2 pt-2 pb-2 px-3 rounded-lg bg-gray-100 w-full ${s.settingsTip} text-gray-900`}
  112. value={inputInfo.desc}
  113. onChange={onChange('desc')}
  114. placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string}
  115. />
  116. <div className={`mt-6 mb-2 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.language`)}</div>
  117. <SimpleSelect
  118. items={Object.keys(LANGUAGE_MAP).map(lang => ({ name: LANGUAGE_MAP[lang as Language], value: lang }))}
  119. defaultValue={language}
  120. onSelect={item => setLanguage(item.value as Language)}
  121. />
  122. {!isShowMore && <div className='w-full cursor-pointer mt-8' onClick={() => setIsShowMore(true)}>
  123. <div className='flex justify-between'>
  124. <div className={`font-medium ${s.settingTitle} flex-grow text-gray-900`}>{t(`${prefixSettings}.more.entry`)}</div>
  125. <div className='flex-shrink-0 w-4 h-4 text-gray-500'>
  126. <ChevronRightIcon />
  127. </div>
  128. </div>
  129. <p className={`mt-1 ${s.policy} text-gray-500`}>{t(`${prefixSettings}.more.copyright`)} & {t(`${prefixSettings}.more.privacyPolicy`)}</p>
  130. </div>}
  131. {isShowMore && <>
  132. <hr className='w-full mt-6' />
  133. <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.copyright`)}</div>
  134. <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
  135. value={inputInfo.copyright}
  136. onChange={onChange('copyright')}
  137. placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`) as string}
  138. />
  139. <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.privacyPolicy`)}</div>
  140. <p className={`mt-1 ${s.settingsTip} text-gray-500`}>
  141. <Trans
  142. i18nKey={`${prefixSettings}.more.privacyPolicyTip`}
  143. components={{ privacyPolicyLink: <Link href={'https://langgenius.ai/privacy-policy'} target='_blank' className='text-primary-600' /> }}
  144. />
  145. </p>
  146. <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
  147. value={inputInfo.privacyPolicy}
  148. onChange={onChange('privacyPolicy')}
  149. placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`) as string}
  150. />
  151. </>}
  152. <div className='mt-10 flex justify-end'>
  153. <Button className='mr-2 flex-shrink-0' onClick={onHide}>{t('common.operation.cancel')}</Button>
  154. <Button type='primary' className='flex-shrink-0' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button>
  155. </div>
  156. {showEmojiPicker && <EmojiPicker
  157. onSelect={(icon, icon_background) => {
  158. setEmoji({ icon, icon_background })
  159. setShowEmojiPicker(false)
  160. }}
  161. onClose={() => {
  162. setEmoji({ icon: appInfo.site.icon, icon_background: appInfo.site.icon_background })
  163. setShowEmojiPicker(false)
  164. }}
  165. />}
  166. </Modal >
  167. </>
  168. )
  169. }
  170. export default React.memo(SettingsModal)