forked from Archives/Athou_commafeed
add search support
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Box, createStyles, TypographyStylesProvider } from "@mantine/core"
|
||||
import { Box, createStyles, Mark, TypographyStylesProvider } from "@mantine/core"
|
||||
import { Constants } from "app/constants"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { calculatePlaceholderSize } from "app/utils"
|
||||
import { ImageWithPlaceholderWhileLoading } from "components/ImageWithPlaceholderWhileLoading"
|
||||
import { Interweave, TransformCallback } from "interweave"
|
||||
import { ChildrenNode, Interweave, Matcher, MatchResponse, Node, TransformCallback } from "interweave"
|
||||
|
||||
export interface ContentProps {
|
||||
content: string
|
||||
@@ -54,13 +55,39 @@ const transform: TransformCallback = node => {
|
||||
return undefined
|
||||
}
|
||||
|
||||
class HighlightMatcher extends Matcher {
|
||||
private search: string
|
||||
|
||||
constructor(search: string) {
|
||||
super("highlight")
|
||||
this.search = search
|
||||
}
|
||||
|
||||
match(string: string): MatchResponse<unknown> | null {
|
||||
const pattern = this.search.split(" ").join("|")
|
||||
return this.doMatch(string, new RegExp(pattern, "i"), () => ({}))
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
|
||||
replaceWith(children: ChildrenNode, props: unknown): Node {
|
||||
return <Mark>{children}</Mark>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
asTag(): string {
|
||||
return "span"
|
||||
}
|
||||
}
|
||||
|
||||
export function Content(props: ContentProps) {
|
||||
const { classes } = useStyles()
|
||||
const search = useAppSelector(state => state.entries.search)
|
||||
const matchers = search ? [new HighlightMatcher(search)] : []
|
||||
|
||||
return (
|
||||
<TypographyStylesProvider>
|
||||
<Box className={classes.content}>
|
||||
<Interweave content={props.content} transform={transform} />
|
||||
<Interweave content={props.content} transform={transform} matchers={matchers} />
|
||||
</Box>
|
||||
</TypographyStylesProvider>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Box, createStyles, Image, Text } from "@mantine/core"
|
||||
import { Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { OnDesktop } from "components/responsive/OnDesktop"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
|
||||
export interface FeedEntryHeaderProps {
|
||||
entry: Entry
|
||||
@@ -43,7 +44,9 @@ export function FeedEntryCompactHeader(props: FeedEntryHeaderProps) {
|
||||
{props.entry.feedName}
|
||||
</Text>
|
||||
</OnDesktop>
|
||||
<Box className={classes.title}>{props.entry.title}</Box>
|
||||
<Box className={classes.title}>
|
||||
<FeedEntryTitle entry={props.entry} />
|
||||
</Box>
|
||||
<OnDesktop>
|
||||
<Text color="dimmed" className={classes.date}>
|
||||
<RelativeDate date={props.entry.date} />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Box, createStyles, Image, Text } from "@mantine/core"
|
||||
import { Entry } from "app/types"
|
||||
import { RelativeDate } from "components/RelativeDate"
|
||||
import { FeedEntryTitle } from "./FeedEntryTitle"
|
||||
|
||||
export interface FeedEntryHeaderProps {
|
||||
entry: Entry
|
||||
@@ -27,7 +28,9 @@ export function FeedEntryHeader(props: FeedEntryHeaderProps) {
|
||||
const { classes } = useStyles(props)
|
||||
return (
|
||||
<Box>
|
||||
<Box className={classes.headerText}>{props.entry.title}</Box>
|
||||
<Box className={classes.headerText}>
|
||||
<FeedEntryTitle entry={props.entry} />
|
||||
</Box>
|
||||
<Box className={classes.headerSubtext}>
|
||||
<Box mr={6}>
|
||||
<Image withPlaceholder src={props.entry.iconUrl} alt="feed icon" width={18} height={18} />
|
||||
|
||||
13
commafeed-client/src/components/content/FeedEntryTitle.tsx
Normal file
13
commafeed-client/src/components/content/FeedEntryTitle.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Highlight } from "@mantine/core"
|
||||
import { useAppSelector } from "app/store"
|
||||
import { Entry } from "app/types"
|
||||
|
||||
export interface FeedEntryTitleProps {
|
||||
entry: Entry
|
||||
}
|
||||
|
||||
export function FeedEntryTitle(props: FeedEntryTitleProps) {
|
||||
const search = useAppSelector(state => state.entries.search)
|
||||
const keywords = search?.split(" ")
|
||||
return <Highlight highlight={keywords ?? ""}>{props.entry.title}</Highlight>
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { t } from "@lingui/macro"
|
||||
import { Center, Divider, Group } from "@mantine/core"
|
||||
import { reloadEntries } from "app/slices/entries"
|
||||
import { Center, Divider, Group, Indicator, Popover, TextInput } from "@mantine/core"
|
||||
import { useForm } from "@mantine/form"
|
||||
import { reloadEntries, search } from "app/slices/entries"
|
||||
import { changeReadingMode, changeReadingOrder } from "app/slices/user"
|
||||
import { useAppDispatch, useAppSelector } from "app/store"
|
||||
import { ActionButton } from "components/ActionButtton"
|
||||
import { Loader } from "components/Loader"
|
||||
import { TbArrowDown, TbArrowUp, TbEye, TbEyeOff, TbRefresh, TbUser } from "react-icons/tb"
|
||||
import { useEffect } from "react"
|
||||
import { TbArrowDown, TbArrowUp, TbEye, TbEyeOff, TbRefresh, TbSearch, TbUser } from "react-icons/tb"
|
||||
import { MarkAllAsReadButton } from "./MarkAllAsReadButton"
|
||||
import { ProfileMenu } from "./ProfileMenu"
|
||||
|
||||
@@ -17,8 +19,22 @@ const iconSize = 18
|
||||
export function Header() {
|
||||
const settings = useAppSelector(state => state.user.settings)
|
||||
const profile = useAppSelector(state => state.user.profile)
|
||||
const searchFromStore = useAppSelector(state => state.entries.search)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const searchForm = useForm<{ search: string }>({
|
||||
validate: {
|
||||
search: value => (value.length > 0 && value.length < 3 ? t`Search requires at least 3 characters` : null),
|
||||
},
|
||||
})
|
||||
const { setValues } = searchForm
|
||||
|
||||
useEffect(() => {
|
||||
setValues({
|
||||
search: searchFromStore,
|
||||
})
|
||||
}, [setValues, searchFromStore])
|
||||
|
||||
if (!settings) return <Loader />
|
||||
return (
|
||||
<Center>
|
||||
@@ -39,6 +55,24 @@ export function Header() {
|
||||
onClick={() => dispatch(changeReadingOrder(settings.readingOrder === "asc" ? "desc" : "asc"))}
|
||||
/>
|
||||
|
||||
<Popover>
|
||||
<Popover.Target>
|
||||
<Indicator disabled={!searchFromStore}>
|
||||
<ActionButton icon={<TbSearch size={iconSize} />} label={t`Search`} />
|
||||
</Indicator>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<form onSubmit={searchForm.onSubmit(values => dispatch(search(values.search)))}>
|
||||
<TextInput
|
||||
placeholder={t`Search`}
|
||||
{...searchForm.getInputProps("search")}
|
||||
icon={<TbSearch size={iconSize} />}
|
||||
autoFocus
|
||||
/>
|
||||
</form>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
<HeaderDivider />
|
||||
|
||||
<ProfileMenu control={<ActionButton icon={<TbUser size={iconSize} />} label={profile?.name} />} />
|
||||
|
||||
Reference in New Issue
Block a user