diff --git a/src/components/settingItem.tsx b/src/components/settingItem.tsx index 94bebb1..921bda5 100644 --- a/src/components/settingItem.tsx +++ b/src/components/settingItem.tsx @@ -9,6 +9,7 @@ import { SelectValue, SelectItem as _SelectItem, } from "./ui/select"; +import { Input } from "./ui/input"; type ItemBase = { label: string; @@ -123,4 +124,17 @@ function EntryItem({ ); } -export { Label, Description, Header, Item, SelectItem, SwitchItem, EntryItem }; +function InputItem({ + label, + description, + className, + ...props +}: ItemBase & Omit, "className">) { + return ( + + + + ); +} + +export { Label, Description, Header, Item, SelectItem, SwitchItem, EntryItem, InputItem }; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..ae2f21c --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,24 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export type InputProps = React.InputHTMLAttributes; + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; + +export { Input }; diff --git a/src/features/header/index.tsx b/src/features/header/index.tsx index 646546d..a8c9a91 100644 --- a/src/features/header/index.tsx +++ b/src/features/header/index.tsx @@ -7,6 +7,7 @@ import { IoLogoGithub } from "react-icons/io"; import { BiMenu } from "react-icons/bi"; import { ToggleButton } from "@/components/toggleButton"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { SettingMenu } from "../settingMenu"; import { useHeader } from "./viewModel"; @@ -18,6 +19,7 @@ export function Header() { marqueeTitleState, displayHistoryState, filterState, + titleFilter, isDesktop, } = useHeader(); const cn = isScrolled ? "shadow-lg border-b" : "shadow-none"; @@ -35,6 +37,13 @@ export function Header() {
Vspo stream schedule
+ titleFilter.onChange(e.target.value)} + className="max-w-xs ml-2 hidden md:block" + />
{isDesktop && (
diff --git a/src/features/header/viewModel.ts b/src/features/header/viewModel.ts index b394918..e2054cf 100644 --- a/src/features/header/viewModel.ts +++ b/src/features/header/viewModel.ts @@ -5,7 +5,7 @@ import { useMediaQuery } from "react-responsive"; export function useHeader() { const [isScrolled, setIsScrolled] = useState(false); - const { theme, isMarqueeTitle, isDisplayHistory, filteredStreamerIds } = + const { theme, isMarqueeTitle, isDisplayHistory, filteredStreamerIds, filteredTitle } = useSettings(); const dispatch = useSettingDispatch(); const isDesktop = useMediaQuery({ query: "(min-width: 768px)" }); @@ -64,6 +64,16 @@ export function useHeader() { description: "Filter by streamer", }; + const titleFilter = { + value: filteredTitle, + onChange: (value: string) => { + dispatch({ + target: "filteredTitle", + payload: value, + }); + }, + }; + return { isScrolled, onClickGithubIcon, @@ -71,6 +81,7 @@ export function useHeader() { marqueeTitleState, displayHistoryState, filterState, + titleFilter, isDesktop, }; } diff --git a/src/providers/setting/schema.ts b/src/providers/setting/schema.ts index c94803b..ca32515 100644 --- a/src/providers/setting/schema.ts +++ b/src/providers/setting/schema.ts @@ -5,6 +5,7 @@ export const SettingSchema = z.object({ isMarqueeTitle: z.boolean().default(false), isDisplayHistory: z.boolean().default(false), filteredStreamerIds: z.array(z.string()).default([]), + filteredTitle: z.string().default(""), }); export type Setting = z.infer; @@ -26,6 +27,6 @@ type ClearAction = { }; export type SettingAction = - | Action<"theme" | "isMarqueeTitle" | "isDisplayHistory"> + | Action<"theme" | "isMarqueeTitle" | "isDisplayHistory" | "filteredTitle"> | FilterAction<"filteredStreamerIds"> | ClearAction<"filteredStreamerIds">; diff --git a/src/providers/vspoStream/provider.tsx b/src/providers/vspoStream/provider.tsx index c2d0bc0..bfaee23 100644 --- a/src/providers/vspoStream/provider.tsx +++ b/src/providers/vspoStream/provider.tsx @@ -47,7 +47,7 @@ const parseToStreamer = ( export const VspoStreamProvider = ({ children }: { children: ReactNode }) => { const [streamResponses, setStreamsResponse] = useState([]); const [streamerMap, setStreamerMap] = useState({}); - const { filteredStreamerIds } = useSettings(); + const { filteredStreamerIds, filteredTitle } = useSettings(); useEffect(() => { const streamCollectionName = import.meta.env.VITE_STREAM_COLLECTION_NAME; @@ -97,6 +97,8 @@ export const VspoStreamProvider = ({ children }: { children: ReactNode }) => { }, []); const streams = useMemo(() => { + const titleFilterLower = filteredTitle.trim().toLowerCase(); + return streamResponses.reduce((results: Stream[], streamRes) => { const channel = streamerMap[streamRes.streamerId][streamRes.platform]; @@ -113,9 +115,17 @@ export const VspoStreamProvider = ({ children }: { children: ReactNode }) => { return results; } + // filter by title + if ( + titleFilterLower !== "" && + !streamRes.title.toLowerCase().includes(titleFilterLower) + ) { + return results; + } + return results.concat(parseToStream(streamRes, channel)); }, []); - }, [streamResponses, streamerMap, filteredStreamerIds]); + }, [streamResponses, streamerMap, filteredStreamerIds, filteredTitle]); const streamers = useMemo( () => Object.values(streamerMap),