From 7255bede40d3e0158d6410d72b18789ad42b367c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Dec 2025 11:00:59 +0000 Subject: [PATCH 1/3] Initial plan From 17713ea1e181f6876217bb92b09978d9dc6ad37c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Dec 2025 11:12:45 +0000 Subject: [PATCH 2/3] Add filter by stream title textfield in header Co-authored-by: sarisia <33576079+sarisia@users.noreply.github.com> --- src/components/settingItem.tsx | 16 +++++++++++++++- src/components/ui/input.tsx | 26 ++++++++++++++++++++++++++ src/features/header/index.tsx | 9 +++++++++ src/features/header/viewModel.ts | 13 ++++++++++++- src/providers/setting/schema.ts | 3 ++- src/providers/vspoStream/provider.tsx | 12 ++++++++++-- 6 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 src/components/ui/input.tsx 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..dc8f928 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface InputProps + extends 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..0fbe3e6 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; @@ -113,9 +113,17 @@ export const VspoStreamProvider = ({ children }: { children: ReactNode }) => { return results; } + // filter by title + if ( + filteredTitle.trim() !== "" && + !streamRes.title.toLowerCase().includes(filteredTitle.toLowerCase()) + ) { + return results; + } + return results.concat(parseToStream(streamRes, channel)); }, []); - }, [streamResponses, streamerMap, filteredStreamerIds]); + }, [streamResponses, streamerMap, filteredStreamerIds, filteredTitle]); const streamers = useMemo( () => Object.values(streamerMap), From 87831589616287818d5c3dac2ea569a70d10ad26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Dec 2025 11:15:24 +0000 Subject: [PATCH 3/3] Optimize title filter performance and improve code quality Co-authored-by: sarisia <33576079+sarisia@users.noreply.github.com> --- src/components/ui/input.tsx | 4 +--- src/providers/vspoStream/provider.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index dc8f928..ae2f21c 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -2,9 +2,7 @@ import * as React from "react"; import { cn } from "@/lib/utils"; -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface InputProps - extends React.InputHTMLAttributes {} +export type InputProps = React.InputHTMLAttributes; const Input = React.forwardRef( ({ className, type, ...props }, ref) => { diff --git a/src/providers/vspoStream/provider.tsx b/src/providers/vspoStream/provider.tsx index 0fbe3e6..bfaee23 100644 --- a/src/providers/vspoStream/provider.tsx +++ b/src/providers/vspoStream/provider.tsx @@ -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]; @@ -115,8 +117,8 @@ export const VspoStreamProvider = ({ children }: { children: ReactNode }) => { // filter by title if ( - filteredTitle.trim() !== "" && - !streamRes.title.toLowerCase().includes(filteredTitle.toLowerCase()) + titleFilterLower !== "" && + !streamRes.title.toLowerCase().includes(titleFilterLower) ) { return results; }