Skip to content
This repository was archived by the owner on Aug 8, 2025. It is now read-only.

Commit 54d0389

Browse files
committed
[search]: added fuse.js for fuzzy search;
1 parent 9f7ac35 commit 54d0389

File tree

1 file changed

+131
-67
lines changed

1 file changed

+131
-67
lines changed

src/layout/CustomLayout.tsx

Lines changed: 131 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import React, { ReactNode, useEffect, useState } from 'react';
22
import type { MenuDataItem } from '@ant-design/pro-components';
33
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';
66
import { copyText } from 'copy-clipboard-js';
77
import CopyOutlined from '@ant-design/icons/CopyOutlined';
88
import { ReloadOutlined, SearchOutlined } from '@ant-design/icons';
9+
import Fuse from 'fuse.js';
910
import logo from './logo.svg';
1011

1112

@@ -823,83 +824,146 @@ const CustomFooterMenu = ({ collapsed }: ICustomFooterMenuProps) => {
823824
);
824825
};
825826

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);
833833
};
834+
};
835+
836+
const contexts = import.meta.glob('../dump/*/index.json');
837+
838+
const SearchBar = () => {
839+
const navigate = useNavigate();
834840
const [form] = Form.useForm();
841+
const sections = ['whole']; // todo: add sections dynamically, news, discussion, etc.
842+
835843
const [resetVisibility, setResetVisibility] = useState(true);
844+
const [results, setResults] = useState<any[]>([]);
845+
const [loading, setLoading] = useState(true);
846+
const [fuse, setFuse] = useState<any>(null);
836847

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+
}
842899
};
843900

844901
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+
}}
862915
>
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>
869922

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
894944
style={{
895-
background: '#1677ff',
945+
marginTop: 8,
946+
background: '#fff',
947+
border: '1px solid #ddd',
948+
maxHeight: 250,
949+
overflowY: 'auto',
896950
}}
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+
)}
900964
/>
901-
</Form.Item>
902-
</Form>
965+
) : null}
966+
</div>
903967
);
904968
};
905969

0 commit comments

Comments
 (0)