search-input.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import {
  2. useCallback,
  3. useRef,
  4. useState,
  5. } from 'react'
  6. import { useTranslation } from 'react-i18next'
  7. import { useEducation } from './hooks'
  8. import Input from '@/app/components/base/input'
  9. import {
  10. PortalToFollowElem,
  11. PortalToFollowElemContent,
  12. PortalToFollowElemTrigger,
  13. } from '@/app/components/base/portal-to-follow-elem'
  14. type SearchInputProps = {
  15. value?: string
  16. onChange: (value: string) => void
  17. }
  18. const SearchInput = ({
  19. value,
  20. onChange,
  21. }: SearchInputProps) => {
  22. const { t } = useTranslation()
  23. const [open, setOpen] = useState(false)
  24. const {
  25. schools,
  26. setSchools,
  27. querySchoolsWithDebounced,
  28. handleUpdateSchools,
  29. hasNext,
  30. } = useEducation()
  31. const pageRef = useRef(0)
  32. const valueRef = useRef(value)
  33. const handleSearch = useCallback((debounced?: boolean) => {
  34. const keywords = valueRef.current
  35. const page = pageRef.current
  36. if (debounced) {
  37. querySchoolsWithDebounced({
  38. keywords,
  39. page,
  40. })
  41. return
  42. }
  43. handleUpdateSchools({
  44. keywords,
  45. page,
  46. })
  47. }, [querySchoolsWithDebounced, handleUpdateSchools])
  48. const handleValueChange = useCallback((e: any) => {
  49. setOpen(true)
  50. setSchools([])
  51. pageRef.current = 0
  52. const inputValue = e.target.value
  53. valueRef.current = inputValue
  54. onChange(inputValue)
  55. handleSearch(true)
  56. }, [onChange, handleSearch, setSchools])
  57. const handleScroll = useCallback((e: Event) => {
  58. const target = e.target as HTMLDivElement
  59. const {
  60. scrollTop,
  61. scrollHeight,
  62. clientHeight,
  63. } = target
  64. if (scrollTop + clientHeight >= scrollHeight - 5 && scrollTop > 0 && hasNext) {
  65. pageRef.current += 1
  66. handleSearch()
  67. }
  68. }, [handleSearch, hasNext])
  69. return (
  70. <PortalToFollowElem
  71. open={open}
  72. onOpenChange={setOpen}
  73. placement='bottom'
  74. offset={4}
  75. triggerPopupSameWidth
  76. >
  77. <PortalToFollowElemTrigger className='block w-full'>
  78. <Input
  79. className='w-full'
  80. placeholder={t('education.form.schoolName.placeholder')}
  81. value={value}
  82. onChange={handleValueChange}
  83. />
  84. </PortalToFollowElemTrigger>
  85. <PortalToFollowElemContent className='z-[32]'>
  86. {
  87. !!schools.length && value && (
  88. <div
  89. className='max-h-[330px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1'
  90. onScroll={handleScroll as any}
  91. >
  92. {
  93. schools.map((school, index) => (
  94. <div
  95. key={index}
  96. className='system-md-regular flex h-8 cursor-pointer items-center truncate rounded-lg px-2 py-1.5 text-text-secondary hover:bg-state-base-hover'
  97. title={school}
  98. onClick={() => {
  99. onChange(school)
  100. setOpen(false)
  101. }}
  102. >
  103. {school}
  104. </div>
  105. ))
  106. }
  107. </div>
  108. )
  109. }
  110. </PortalToFollowElemContent>
  111. </PortalToFollowElem>
  112. )
  113. }
  114. export default SearchInput