mirror of
https://github.com/pawelmalak/snippet-box.git
synced 2025-12-21 13:23:05 +01:00
Search for snippet by title, description and language
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import { useRef, useEffect, KeyboardEvent } from 'react';
|
import { useRef, useEffect, KeyboardEvent, useContext } from 'react';
|
||||||
|
import { SnippetsContext } from '../store';
|
||||||
import { searchParser } from '../utils';
|
import { searchParser } from '../utils';
|
||||||
|
|
||||||
export const SearchBar = (): JSX.Element => {
|
export const SearchBar = (): JSX.Element => {
|
||||||
|
const { searchSnippets } = useContext(SnippetsContext);
|
||||||
const inputRef = useRef<HTMLInputElement>(document.createElement('input'));
|
const inputRef = useRef<HTMLInputElement>(document.createElement('input'));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -9,7 +11,11 @@ export const SearchBar = (): JSX.Element => {
|
|||||||
}, [inputRef]);
|
}, [inputRef]);
|
||||||
|
|
||||||
const inputHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
const inputHandler = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
const rawQuery = searchParser(inputRef.current.value);
|
const query = searchParser(inputRef.current.value);
|
||||||
|
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
searchSnippets(query);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { SnippetGrid } from '../components/Snippets/SnippetGrid';
|
|||||||
import { SearchBar } from '../components/SearchBar';
|
import { SearchBar } from '../components/SearchBar';
|
||||||
|
|
||||||
export const Home = (): JSX.Element => {
|
export const Home = (): JSX.Element => {
|
||||||
const { snippets, getSnippets } = useContext(SnippetsContext);
|
const { snippets, getSnippets, searchResults } = useContext(SnippetsContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getSnippets();
|
getSnippets();
|
||||||
@@ -19,9 +19,9 @@ export const Home = (): JSX.Element => {
|
|||||||
<Fragment>
|
<Fragment>
|
||||||
<PageHeader title='Search' />
|
<PageHeader title='Search' />
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
{/* <div className='col-12 mb-4'>
|
<div className='col-12 mb-4'>
|
||||||
<SnippetGrid snippets={snippets.filter(s => s.isPinned)} />
|
<SnippetGrid snippets={searchResults} />
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
{snippets.some(s => s.isPinned) && (
|
{snippets.some(s => s.isPinned) && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import {
|
|||||||
Snippet,
|
Snippet,
|
||||||
Response,
|
Response,
|
||||||
TagCount,
|
TagCount,
|
||||||
NewSnippet
|
NewSnippet,
|
||||||
|
SearchQuery
|
||||||
} from '../typescript/interfaces';
|
} from '../typescript/interfaces';
|
||||||
|
|
||||||
export const SnippetsContext = createContext<Context>({
|
export const SnippetsContext = createContext<Context>({
|
||||||
snippets: [],
|
snippets: [],
|
||||||
|
searchResults: [],
|
||||||
currentSnippet: null,
|
currentSnippet: null,
|
||||||
tagCount: [],
|
tagCount: [],
|
||||||
getSnippets: () => {},
|
getSnippets: () => {},
|
||||||
@@ -20,7 +22,8 @@ export const SnippetsContext = createContext<Context>({
|
|||||||
updateSnippet: (snippet: NewSnippet, id: number, isLocal?: boolean) => {},
|
updateSnippet: (snippet: NewSnippet, id: number, isLocal?: boolean) => {},
|
||||||
deleteSnippet: (id: number) => {},
|
deleteSnippet: (id: number) => {},
|
||||||
toggleSnippetPin: (id: number) => {},
|
toggleSnippetPin: (id: number) => {},
|
||||||
countTags: () => {}
|
countTags: () => {},
|
||||||
|
searchSnippets: (query: SearchQuery) => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -29,6 +32,7 @@ interface Props {
|
|||||||
|
|
||||||
export const SnippetsContextProvider = (props: Props): JSX.Element => {
|
export const SnippetsContextProvider = (props: Props): JSX.Element => {
|
||||||
const [snippets, setSnippets] = useState<Snippet[]>([]);
|
const [snippets, setSnippets] = useState<Snippet[]>([]);
|
||||||
|
const [searchResults, setSearchResults] = useState<Snippet[]>([]);
|
||||||
const [currentSnippet, setCurrentSnippet] = useState<Snippet | null>(null);
|
const [currentSnippet, setCurrentSnippet] = useState<Snippet | null>(null);
|
||||||
const [tagCount, setTagCount] = useState<TagCount[]>([]);
|
const [tagCount, setTagCount] = useState<TagCount[]>([]);
|
||||||
|
|
||||||
@@ -139,8 +143,19 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
|
|||||||
.catch(err => redirectOnError());
|
.catch(err => redirectOnError());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchSnippets = (query: SearchQuery): void => {
|
||||||
|
axios
|
||||||
|
.post<Response<Snippet[]>>('/api/snippets/search', query)
|
||||||
|
.then(res => {
|
||||||
|
setSearchResults(res.data.data);
|
||||||
|
console.log(res.data.data);
|
||||||
|
})
|
||||||
|
.catch(err => console.log(err));
|
||||||
|
};
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
snippets,
|
snippets,
|
||||||
|
searchResults,
|
||||||
currentSnippet,
|
currentSnippet,
|
||||||
tagCount,
|
tagCount,
|
||||||
getSnippets,
|
getSnippets,
|
||||||
@@ -150,7 +165,8 @@ export const SnippetsContextProvider = (props: Props): JSX.Element => {
|
|||||||
updateSnippet,
|
updateSnippet,
|
||||||
deleteSnippet,
|
deleteSnippet,
|
||||||
toggleSnippetPin,
|
toggleSnippetPin,
|
||||||
countTags
|
countTags,
|
||||||
|
searchSnippets
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { TagCount, NewSnippet, Snippet } from '.';
|
import { TagCount, NewSnippet, Snippet, SearchQuery } from '.';
|
||||||
|
|
||||||
export interface Context {
|
export interface Context {
|
||||||
snippets: Snippet[];
|
snippets: Snippet[];
|
||||||
|
searchResults: Snippet[];
|
||||||
currentSnippet: Snippet | null;
|
currentSnippet: Snippet | null;
|
||||||
tagCount: TagCount[];
|
tagCount: TagCount[];
|
||||||
getSnippets: () => void;
|
getSnippets: () => void;
|
||||||
@@ -12,4 +13,5 @@ export interface Context {
|
|||||||
deleteSnippet: (id: number) => void;
|
deleteSnippet: (id: number) => void;
|
||||||
toggleSnippetPin: (id: number) => void;
|
toggleSnippetPin: (id: number) => void;
|
||||||
countTags: () => void;
|
countTags: () => void;
|
||||||
|
searchSnippets: (query: SearchQuery) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
5
client/src/typescript/interfaces/SearchQuery.ts
Normal file
5
client/src/typescript/interfaces/SearchQuery.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface SearchQuery {
|
||||||
|
query: string;
|
||||||
|
tags: string[];
|
||||||
|
languages: string[];
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@ export * from './Route';
|
|||||||
export * from './Response';
|
export * from './Response';
|
||||||
export * from './Context';
|
export * from './Context';
|
||||||
export * from './Statistics';
|
export * from './Statistics';
|
||||||
|
export * from './SearchQuery';
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
export const searchParser = (rawQuery: string): void => {
|
import { SearchQuery } from '../typescript/interfaces';
|
||||||
// const rawQuery = 'my search tags:ui,react lang:typescript';
|
|
||||||
|
|
||||||
|
export const searchParser = (rawQuery: string): SearchQuery => {
|
||||||
// Extract filters from query
|
// Extract filters from query
|
||||||
const tags = extractFilters(rawQuery, 'tags');
|
const tags = extractFilters(rawQuery, 'tags');
|
||||||
const languages = extractFilters(rawQuery, 'lang');
|
const languages = extractFilters(rawQuery, 'lang');
|
||||||
const query = rawQuery.replaceAll(/(tags|lang):[a-zA-Z]+(,[a-zA-Z]+)*/g, '');
|
const query = rawQuery.replaceAll(/(tags|lang):[a-zA-Z]+(,[a-zA-Z]+)*/g, '');
|
||||||
|
|
||||||
console.log(tags);
|
return {
|
||||||
console.log(languages);
|
query: query.trim(),
|
||||||
console.log(query);
|
tags,
|
||||||
|
languages
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractFilters = (query: string, filter: string): string[] => {
|
const extractFilters = (query: string, filter: string): string[] => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { QueryTypes } from 'sequelize';
|
import { QueryTypes, Op } from 'sequelize';
|
||||||
import { sequelize } from '../db';
|
import { sequelize } from '../db';
|
||||||
import { asyncWrapper } from '../middleware';
|
import { asyncWrapper } from '../middleware';
|
||||||
import { SnippetModel, Snippet_TagModel } from '../models';
|
import { SnippetModel, Snippet_TagModel } from '../models';
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Logger,
|
Logger,
|
||||||
createTags
|
createTags
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { Body } from '../typescript/interfaces';
|
import { Body, SearchQuery } from '../typescript/interfaces';
|
||||||
|
|
||||||
const logger = new Logger('snippets-controller');
|
const logger = new Logger('snippets-controller');
|
||||||
|
|
||||||
@@ -209,3 +209,39 @@ export const countTags = asyncWrapper(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Search snippets
|
||||||
|
* @route /api/snippets/search
|
||||||
|
* @request POST
|
||||||
|
*/
|
||||||
|
export const searchSnippets = asyncWrapper(
|
||||||
|
async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||||
|
const { query, tags, languages } = <SearchQuery>req.body;
|
||||||
|
|
||||||
|
console.log(query, tags, languages);
|
||||||
|
|
||||||
|
const languageFilter =
|
||||||
|
languages.length > 0 ? { [Op.in]: languages } : { [Op.notIn]: languages };
|
||||||
|
|
||||||
|
const snippets = await SnippetModel.findAll({
|
||||||
|
where: {
|
||||||
|
[Op.and]: [
|
||||||
|
{
|
||||||
|
[Op.or]: [
|
||||||
|
{ title: { [Op.substring]: `${query}` } },
|
||||||
|
{ description: { [Op.substring]: `${query}` } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
language: languageFilter
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
data: snippets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
deleteSnippet,
|
deleteSnippet,
|
||||||
getAllSnippets,
|
getAllSnippets,
|
||||||
getSnippet,
|
getSnippet,
|
||||||
|
searchSnippets,
|
||||||
updateSnippet
|
updateSnippet
|
||||||
} from '../controllers/snippets';
|
} from '../controllers/snippets';
|
||||||
import { requireBody } from '../middleware';
|
import { requireBody } from '../middleware';
|
||||||
@@ -23,3 +24,4 @@ snippetRouter
|
|||||||
.delete(deleteSnippet);
|
.delete(deleteSnippet);
|
||||||
|
|
||||||
snippetRouter.route('/statistics/count').get(countTags);
|
snippetRouter.route('/statistics/count').get(countTags);
|
||||||
|
snippetRouter.route('/search').post(searchSnippets);
|
||||||
|
|||||||
5
src/typescript/interfaces/SearchQuery.ts
Normal file
5
src/typescript/interfaces/SearchQuery.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface SearchQuery {
|
||||||
|
query: string;
|
||||||
|
tags: string[];
|
||||||
|
languages: string[];
|
||||||
|
}
|
||||||
@@ -3,3 +3,4 @@ export * from './Snippet';
|
|||||||
export * from './Tag';
|
export * from './Tag';
|
||||||
export * from './Snippet_Tag';
|
export * from './Snippet_Tag';
|
||||||
export * from './Body';
|
export * from './Body';
|
||||||
|
export * from './SearchQuery';
|
||||||
|
|||||||
Reference in New Issue
Block a user