|
1 | 1 | import React, { ReactNode, useEffect, useState } from 'react'; |
2 | 2 | import type { MenuDataItem } from '@ant-design/pro-components'; |
3 | 3 | import { PageContainer, ProLayout } from '@ant-design/pro-components'; |
4 | | -import { useLocation, Link } from 'react-router-dom'; |
5 | | -import { notification, Input, Select, Form, Button, Affix } from 'antd'; |
| 4 | +import { useLocation, Link, useNavigate } from 'react-router-dom'; |
| 5 | +import { notification, Input, Select, Form, Button, Affix, List, Spin } from 'antd'; |
6 | 6 | import { copyText } from 'copy-clipboard-js'; |
7 | 7 | import CopyOutlined from '@ant-design/icons/CopyOutlined'; |
8 | 8 | import { ReloadOutlined, SearchOutlined } from '@ant-design/icons'; |
| 9 | +import Fuse from 'fuse.js'; |
9 | 10 | import logo from './logo.svg'; |
10 | 11 |
|
11 | 12 |
|
@@ -823,83 +824,146 @@ const CustomFooterMenu = ({ collapsed }: ICustomFooterMenuProps) => { |
823 | 824 | ); |
824 | 825 | }; |
825 | 826 |
|
826 | | -const SearchBar = () => { |
827 | | - const sections = ['whole', 'section1', 'section2', 'section3']; |
828 | | - const onFinish = (values: any) => { |
829 | | - console.log('Success:', values); |
830 | | - }; |
831 | | - const onFinishFailed = (errorInfo: any) => { |
832 | | - console.log('Failed:', errorInfo); |
| 827 | + |
| 828 | +const debounce = (func: (...args: any[]) => void, wait: number) => { |
| 829 | + let timeout: NodeJS.Timeout; |
| 830 | + return (...args: any[]) => { |
| 831 | + clearTimeout(timeout); |
| 832 | + timeout = setTimeout(() => func.apply(this, args), wait); |
833 | 833 | }; |
| 834 | +}; |
| 835 | + |
| 836 | +const contexts = import.meta.glob('../dump/*/index.json'); |
| 837 | + |
| 838 | +const SearchBar = () => { |
| 839 | + const navigate = useNavigate(); |
834 | 840 | const [form] = Form.useForm(); |
| 841 | + const sections = ['whole']; // todo: add sections dynamically, news, discussion, etc. |
| 842 | + |
835 | 843 | const [resetVisibility, setResetVisibility] = useState(true); |
| 844 | + const [results, setResults] = useState<any[]>([]); |
| 845 | + const [loading, setLoading] = useState(true); |
| 846 | + const [fuse, setFuse] = useState<any>(null); |
836 | 847 |
|
837 | | - const onValuesChange = (changedValues: any) => { |
838 | | - console.log('Form changed:', changedValues); |
839 | | - const { range, keyword } = changedValues; |
840 | | - console.log('Search triggered with:', { range, keyword }); |
841 | | - setResetVisibility(range === undefined && (keyword === undefined || keyword === '')); |
| 848 | + useEffect(() => { |
| 849 | + const loadData = async () => { |
| 850 | + setLoading(true); |
| 851 | + const posts: any[] = []; |
| 852 | + for (const path in contexts) { |
| 853 | + const data = await contexts[path](); |
| 854 | + const postId = path.split('/').slice(-2, -1)[0]; |
| 855 | + // @ts-expect-error reddit should send data in a the following format |
| 856 | + const title = data?.[0]?.data?.children?.[0]?.data?.title || postId; |
| 857 | + // @ts-expect-error reddit should send data in a the following format |
| 858 | + const body = data?.[0]?.data?.children?.[0]?.data?.selftext || ''; |
| 859 | + posts.push({ id: postId, title, body }); |
| 860 | + } |
| 861 | + |
| 862 | + const fuseInstance = new Fuse(posts, { |
| 863 | + keys: ['title', 'body'], |
| 864 | + threshold: 0.4, |
| 865 | + }); |
| 866 | + setFuse(fuseInstance); |
| 867 | + setLoading(false); |
| 868 | + }; |
| 869 | + |
| 870 | + loadData(); |
| 871 | + }, []); |
| 872 | + |
| 873 | + const doSearch = debounce((keyword: string) => { |
| 874 | + if (fuse && keyword) { |
| 875 | + const searchResults = fuse.search(keyword); |
| 876 | + // @ts-expect-error fuse search returns an array of objects with item property |
| 877 | + setResults(searchResults.map(r => r.item)); |
| 878 | + } else { |
| 879 | + setResults([]); |
| 880 | + } |
| 881 | + }, 30); // 30ms debounce |
| 882 | + |
| 883 | + const onValuesChange = (_changedValues: any, allValues: any) => { |
| 884 | + const { keyword } = allValues; |
| 885 | + setResetVisibility(!keyword); |
| 886 | + if (keyword) { |
| 887 | + doSearch(keyword); |
| 888 | + } else { |
| 889 | + setResults([]); |
| 890 | + } |
| 891 | + }; |
| 892 | + |
| 893 | + const onFinish = () => { |
| 894 | + if (results.length > 0) { |
| 895 | + navigate(`/${results[0].id}`); |
| 896 | + form.resetFields(); |
| 897 | + setResults([]); |
| 898 | + } |
842 | 899 | }; |
843 | 900 |
|
844 | 901 | return ( |
845 | | - <Form |
846 | | - form={form} |
847 | | - layout="inline" |
848 | | - onValuesChange={onValuesChange} |
849 | | - onFinish={onFinish} |
850 | | - onFinishFailed={onFinishFailed} |
851 | | - autoComplete="on" |
852 | | - style={{ |
853 | | - display: 'flex', |
854 | | - padding: 8, |
855 | | - backdropFilter: 'blur(10px)', |
856 | | - backgroundColor: 'rgba(255, 255, 255, 0.5)', |
857 | | - }} |
858 | | - > |
859 | | - <Form.Item |
860 | | - name="range" |
861 | | - rules={[{ required: true, message: 'Please select a range!' }]} |
| 902 | + <div style={{ width: '100%' }}> |
| 903 | + <Form |
| 904 | + form={form} |
| 905 | + layout="inline" |
| 906 | + onValuesChange={onValuesChange} |
| 907 | + onFinish={onFinish} |
| 908 | + autoComplete="on" |
| 909 | + style={{ |
| 910 | + display: 'flex', |
| 911 | + padding: 8, |
| 912 | + backdropFilter: 'blur(10px)', |
| 913 | + backgroundColor: 'rgba(255, 255, 255, 0.5)', |
| 914 | + }} |
862 | 915 | > |
863 | | - <Select |
864 | | - defaultValue={sections[0] || ''} |
865 | | - options={sections.map((section) => ({ label: section, value: section }))} |
866 | | - style={{ width: 120 }} |
867 | | - /> |
868 | | - </Form.Item> |
| 916 | + <Form.Item name="range" initialValue={sections[0]}> |
| 917 | + <Select |
| 918 | + options={sections.map(section => ({ label: section, value: section }))} |
| 919 | + style={{ width: 120 }} |
| 920 | + /> |
| 921 | + </Form.Item> |
869 | 922 |
|
870 | | - <Form.Item |
871 | | - style={{ flex: 1 }} |
872 | | - name="keyword" |
873 | | - rules={[{ required: true, message: 'Please input your keyword!' }]} |
874 | | - > |
875 | | - <Input |
876 | | - placeholder="Search..." |
877 | | - style={{ width: '100%' }} |
878 | | - /> |
879 | | - </Form.Item> |
880 | | - <Form.Item |
881 | | - hidden={resetVisibility} |
882 | | - > |
883 | | - <Button |
884 | | - style={{ |
885 | | - background: '#1677ff', |
886 | | - }} |
887 | | - icon={<ReloadOutlined />} |
888 | | - type="primary" |
889 | | - onClick={() => form.resetFields()} |
890 | | - /> |
891 | | - </Form.Item> |
892 | | - <Form.Item> |
893 | | - <Button |
| 923 | + <Form.Item name="keyword" style={{ flex: 1 }}> |
| 924 | + <Input placeholder="Search..." allowClear /> |
| 925 | + </Form.Item> |
| 926 | + |
| 927 | + <Form.Item hidden={resetVisibility}> |
| 928 | + <Button icon={<ReloadOutlined />} onClick={() => form.resetFields()} /> |
| 929 | + </Form.Item> |
| 930 | + |
| 931 | + <Form.Item> |
| 932 | + <Button icon={<SearchOutlined />} type="primary" htmlType="submit" /> |
| 933 | + </Form.Item> |
| 934 | + </Form> |
| 935 | + |
| 936 | + {loading ? ( |
| 937 | + <div style={{ marginTop: 12, textAlign: 'center' }}> |
| 938 | + <Spin tip="Loading posts for search..." /> |
| 939 | + </div> |
| 940 | + ) : results.length > 0 ? ( |
| 941 | + <List |
| 942 | + size="small" |
| 943 | + bordered |
894 | 944 | style={{ |
895 | | - background: '#1677ff', |
| 945 | + marginTop: 8, |
| 946 | + background: '#fff', |
| 947 | + border: '1px solid #ddd', |
| 948 | + maxHeight: 250, |
| 949 | + overflowY: 'auto', |
896 | 950 | }} |
897 | | - icon={<SearchOutlined />} |
898 | | - type="primary" |
899 | | - htmlType="submit" |
| 951 | + dataSource={results} |
| 952 | + renderItem={item => ( |
| 953 | + <List.Item |
| 954 | + style={{ cursor: 'pointer' }} |
| 955 | + onClick={() => { |
| 956 | + navigate(`/${item.id}`); |
| 957 | + form.resetFields(); |
| 958 | + setResults([]); |
| 959 | + }} |
| 960 | + > |
| 961 | + {item.title} |
| 962 | + </List.Item> |
| 963 | + )} |
900 | 964 | /> |
901 | | - </Form.Item> |
902 | | - </Form> |
| 965 | + ) : null} |
| 966 | + </div> |
903 | 967 | ); |
904 | 968 | }; |
905 | 969 |
|
|
0 commit comments