Skip to content

Commit 43c4f65

Browse files
sarisiaCopilot
andauthored
Add filter by stream title (#81)
* Initial plan * Add filter by stream title textfield in header Co-authored-by: sarisia <33576079+sarisia@users.noreply.github.com> * Optimize title filter performance and improve code quality Co-authored-by: sarisia <33576079+sarisia@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent e767070 commit 43c4f65

6 files changed

Lines changed: 74 additions & 5 deletions

File tree

src/components/settingItem.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
SelectValue,
1010
SelectItem as _SelectItem,
1111
} from "./ui/select";
12+
import { Input } from "./ui/input";
1213

1314
type ItemBase = {
1415
label: string;
@@ -123,4 +124,17 @@ function EntryItem({
123124
);
124125
}
125126

126-
export { Label, Description, Header, Item, SelectItem, SwitchItem, EntryItem };
127+
function InputItem({
128+
label,
129+
description,
130+
className,
131+
...props
132+
}: ItemBase & Omit<ComponentProps<typeof Input>, "className">) {
133+
return (
134+
<Item label={label} description={description} className={className}>
135+
<Input {...props} className="w-48" />
136+
</Item>
137+
);
138+
}
139+
140+
export { Label, Description, Header, Item, SelectItem, SwitchItem, EntryItem, InputItem };

src/components/ui/input.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from "react";
2+
3+
import { cn } from "@/lib/utils";
4+
5+
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
6+
7+
const Input = React.forwardRef<HTMLInputElement, InputProps>(
8+
({ className, type, ...props }, ref) => {
9+
return (
10+
<input
11+
type={type}
12+
className={cn(
13+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
14+
className
15+
)}
16+
ref={ref}
17+
{...props}
18+
/>
19+
);
20+
}
21+
);
22+
Input.displayName = "Input";
23+
24+
export { Input };

src/features/header/index.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IoLogoGithub } from "react-icons/io";
77
import { BiMenu } from "react-icons/bi";
88
import { ToggleButton } from "@/components/toggleButton";
99
import { Button } from "@/components/ui/button";
10+
import { Input } from "@/components/ui/input";
1011
import { SettingMenu } from "../settingMenu";
1112
import { useHeader } from "./viewModel";
1213

@@ -18,6 +19,7 @@ export function Header() {
1819
marqueeTitleState,
1920
displayHistoryState,
2021
filterState,
22+
titleFilter,
2123
isDesktop,
2224
} = useHeader();
2325
const cn = isScrolled ? "shadow-lg border-b" : "shadow-none";
@@ -35,6 +37,13 @@ export function Header() {
3537
<div className="font-[Itim] text-2xl tracking-tighter text-primary hidden sm:block">
3638
Vspo stream schedule
3739
</div>
40+
<Input
41+
type="text"
42+
placeholder="Filter by stream title..."
43+
value={titleFilter.value}
44+
onChange={(e) => titleFilter.onChange(e.target.value)}
45+
className="max-w-xs ml-2 hidden md:block"
46+
/>
3847
<div className="ml-auto flex gap-2">
3948
{isDesktop && (
4049
<div>

src/features/header/viewModel.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useMediaQuery } from "react-responsive";
55

66
export function useHeader() {
77
const [isScrolled, setIsScrolled] = useState(false);
8-
const { theme, isMarqueeTitle, isDisplayHistory, filteredStreamerIds } =
8+
const { theme, isMarqueeTitle, isDisplayHistory, filteredStreamerIds, filteredTitle } =
99
useSettings();
1010
const dispatch = useSettingDispatch();
1111
const isDesktop = useMediaQuery({ query: "(min-width: 768px)" });
@@ -64,13 +64,24 @@ export function useHeader() {
6464
description: "Filter by streamer",
6565
};
6666

67+
const titleFilter = {
68+
value: filteredTitle,
69+
onChange: (value: string) => {
70+
dispatch({
71+
target: "filteredTitle",
72+
payload: value,
73+
});
74+
},
75+
};
76+
6777
return {
6878
isScrolled,
6979
onClickGithubIcon,
7080
themeState,
7181
marqueeTitleState,
7282
displayHistoryState,
7383
filterState,
84+
titleFilter,
7485
isDesktop,
7586
};
7687
}

src/providers/setting/schema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const SettingSchema = z.object({
55
isMarqueeTitle: z.boolean().default(false),
66
isDisplayHistory: z.boolean().default(false),
77
filteredStreamerIds: z.array(z.string()).default([]),
8+
filteredTitle: z.string().default(""),
89
});
910

1011
export type Setting = z.infer<typeof SettingSchema>;
@@ -26,6 +27,6 @@ type ClearAction<K extends keyof Setting> = {
2627
};
2728

2829
export type SettingAction =
29-
| Action<"theme" | "isMarqueeTitle" | "isDisplayHistory">
30+
| Action<"theme" | "isMarqueeTitle" | "isDisplayHistory" | "filteredTitle">
3031
| FilterAction<"filteredStreamerIds">
3132
| ClearAction<"filteredStreamerIds">;

src/providers/vspoStream/provider.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const parseToStreamer = (
4747
export const VspoStreamProvider = ({ children }: { children: ReactNode }) => {
4848
const [streamResponses, setStreamsResponse] = useState<StreamResponse[]>([]);
4949
const [streamerMap, setStreamerMap] = useState<StreamerMap>({});
50-
const { filteredStreamerIds } = useSettings();
50+
const { filteredStreamerIds, filteredTitle } = useSettings();
5151

5252
useEffect(() => {
5353
const streamCollectionName = import.meta.env.VITE_STREAM_COLLECTION_NAME;
@@ -97,6 +97,8 @@ export const VspoStreamProvider = ({ children }: { children: ReactNode }) => {
9797
}, []);
9898

9999
const streams = useMemo<Stream[]>(() => {
100+
const titleFilterLower = filteredTitle.trim().toLowerCase();
101+
100102
return streamResponses.reduce((results: Stream[], streamRes) => {
101103
const channel = streamerMap[streamRes.streamerId][streamRes.platform];
102104

@@ -113,9 +115,17 @@ export const VspoStreamProvider = ({ children }: { children: ReactNode }) => {
113115
return results;
114116
}
115117

118+
// filter by title
119+
if (
120+
titleFilterLower !== "" &&
121+
!streamRes.title.toLowerCase().includes(titleFilterLower)
122+
) {
123+
return results;
124+
}
125+
116126
return results.concat(parseToStream(streamRes, channel));
117127
}, []);
118-
}, [streamResponses, streamerMap, filteredStreamerIds]);
128+
}, [streamResponses, streamerMap, filteredStreamerIds, filteredTitle]);
119129

120130
const streamers = useMemo<Streamer[]>(
121131
() => Object.values(streamerMap),

0 commit comments

Comments
 (0)