Skip to content

Commit 4c60edc

Browse files
RRRyomaKaichiManabeaster-void
authored
ユーザー名検索 (#524)
# PRの概要 検索機能(現在は、ユーザー名のみの部分一致検索) ## 具体的な変更内容 ## 影響範囲 searchタブ内 ## 動作 https://github.com/user-attachments/assets/61db87ab-a4ad-4fea-8b9c-669745bb9713 ## レビューリクエストを出す前にチェック! - [ ✓] 改めてセルフレビューしたか - [ ✓] 手動での動作検証を行ったか - [ ] server の機能追加ならば、テストを書いたか - 理由: 書いた | server の機能追加ではない - [ ] 間違った使い方が存在するならば、それのドキュメントをコメントで書いたか - 理由: 書いた | 間違った使い方は存在しない - [ ] わかりやすいPRになっているか <!-- レビューリクエスト後は、Slackでもメンションしてお願いすることを推奨します。 --> --------- Co-authored-by: KaichiManabe <[email protected]> Co-authored-by: aster <[email protected]>
1 parent 84a13ab commit 4c60edc

File tree

11 files changed

+165
-16
lines changed

11 files changed

+165
-16
lines changed

web/api/user.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import type { Hook as UseHook } from "./share/types.ts";
1010

1111
const UserListSchema = z.array(UserSchema);
1212

13+
export function useAll(): Hook<User[]> {
14+
return useCustomizedSWR("users::all", all, UserListSchema);
15+
}
1316
export function useRecommended(): UseHook<User[]> {
1417
const url = endpoints.recommendedUsers;
1518
return useAuthorizedData<User[]>(url);
@@ -28,6 +31,11 @@ export function usePendingFromMe(): Hook<User[]> {
2831
);
2932
}
3033

34+
async function all(): Promise<User[]> {
35+
const res = await credFetch("GET", endpoints.users);
36+
return res.json();
37+
}
38+
3139
async function matched(): Promise<User[]> {
3240
const res = await credFetch("GET", endpoints.matchedUsers);
3341
return res.json();
@@ -131,6 +139,8 @@ export async function deleteAccount(): Promise<void> {
131139
const res = await credFetch("DELETE", endpoints.me);
132140
if (res.status !== 204)
133141
throw new Error(
134-
`failed to delete account: expected status code 204, but got ${res.status} with text ${await res.text()}`,
142+
`failed to delete account: expected status code 204, but got ${
143+
res.status
144+
} with text ${await res.text()}`,
135145
);
136146
}

web/app/chat/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default function ChatPageLayout({
1313
<div className="absolute top-14 right-0 bottom-14 left-0 overflow-y-auto sm:top-16">
1414
{children}
1515
</div>
16-
<BottomBar activeTab="2_chat" />
16+
<BottomBar activeTab="3_chat" />
1717
</NavigateByAuthState>
1818
);
1919
}

web/app/friends/layout.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import BottomBar from "~/components/BottomBar";
22
import Header from "~/components/Header";
3+
import { NavigateByAuthState } from "~/components/common/NavigateByAuthState";
34

45
export default function FriendsPageLayout({
56
children,
67
}: {
78
children: React.ReactNode;
89
}) {
910
return (
10-
<>
11+
<NavigateByAuthState type="toLoginForUnauthenticated">
1112
<Header title="フレンド/Friends" />
1213
<div className="absolute top-14 right-0 bottom-14 left-0 overflow-y-auto sm:top-16">
1314
{children}
1415
</div>
1516
<BottomBar activeTab="1_friends" />
16-
</>
17+
</NavigateByAuthState>
1718
);
1819
}

web/app/home/layout.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import BottomBar from "~/components/BottomBar";
22
import Header from "~/components/Header";
3+
import { NavigateByAuthState } from "~/components/common/NavigateByAuthState";
34

45
export default function HomePageLayout({
56
children,
67
}: {
78
children: React.ReactNode;
89
}) {
910
return (
10-
<>
11+
<NavigateByAuthState type="toLoginForUnauthenticated">
1112
<Header title="ホーム/Home" />
1213
<div className="relative top-14 right-0 bottom-14 left-0 overflow-y-auto sm:top-16">
1314
{children}
1415
</div>
1516
<BottomBar activeTab="0_home" />
16-
</>
17+
</NavigateByAuthState>
1718
);
1819
}

web/app/search/layout.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import BottomBar from "~/components/BottomBar";
2+
import Header from "~/components/Header";
3+
4+
export default function HomePageLayout({
5+
children,
6+
}: {
7+
children: React.ReactNode;
8+
}) {
9+
return (
10+
<>
11+
<Header title="検索/Search" />
12+
<div className="absolute top-14 right-0 bottom-14 left-0 overflow-y-auto sm:top-16">
13+
{children}
14+
</div>
15+
<BottomBar activeTab="2_search" />
16+
</>
17+
);
18+
}

web/app/search/page.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use client";
2+
3+
import type React from "react";
4+
import { useState } from "react";
5+
import Search from "~/components/search/search";
6+
import Table from "~/components/search/table";
7+
8+
export default function SearchPage({
9+
searchParams,
10+
}: {
11+
searchParams?: {
12+
query?: string;
13+
page?: string;
14+
};
15+
}) {
16+
const [query, setQuery] = useState<string>(searchParams?.query ?? "");
17+
18+
return (
19+
<div className="flex min-h-screen justify-center ">
20+
<div className="w-full">
21+
<h2 className="m-5 mb-4 font-bold text-2xl">ユーザー検索</h2>
22+
<Search placeholder="検索" setSearchString={setQuery} />
23+
<Table query={query} />
24+
</div>
25+
</div>
26+
);
27+
}

web/app/settings/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default function SettingsPageLayout({
1212
<div className="absolute top-14 right-0 bottom-14 left-0 overflow-y-auto sm:top-16">
1313
{children}
1414
</div>
15-
<BottomBar activeTab="3_settings" />
15+
<BottomBar activeTab="4_settings" />
1616
</>
1717
);
1818
}

web/components/BottomBar.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { MdHome } from "react-icons/md";
33
import { MdPeople } from "react-icons/md";
44
import { MdChat } from "react-icons/md";
55
import { MdSettings } from "react-icons/md";
6+
import { MdSearch } from "react-icons/md";
67

78
type Props = {
8-
activeTab: "0_home" | "1_friends" | "2_chat" | "3_settings";
9+
activeTab: "0_home" | "1_friends" | "2_search" | "3_chat" | "4_settings";
910
};
1011

1112
function BottomBarCell({
@@ -22,7 +23,9 @@ function BottomBarCell({
2223
return (
2324
<Link
2425
href={href}
25-
className={`focus:bg-gray-300 ${isActive ? "active text-primary" : "text-secondary"}`}
26+
className={`focus:bg-gray-300 ${
27+
isActive ? "active text-primary" : "text-secondary"
28+
}`}
2629
>
2730
{iconComponent}
2831
<span
@@ -50,16 +53,22 @@ export default function BottomBar(props: Props) {
5053
isActive={activeTab === "1_friends"}
5154
iconComponent={<MdPeople className="text-2xl" />}
5255
/>
56+
<BottomBarCell
57+
label="Search"
58+
href="/search"
59+
isActive={activeTab === "2_search"}
60+
iconComponent={<MdSearch className="text-2xl" />}
61+
/>
5362
<BottomBarCell
5463
label="Chat"
5564
href="/chat"
56-
isActive={activeTab === "2_chat"}
65+
isActive={activeTab === "3_chat"}
5766
iconComponent={<MdChat className="text-2xl" />}
5867
/>
5968
<BottomBarCell
6069
label="Settings"
6170
href="/settings"
62-
isActive={activeTab === "3_settings"}
71+
isActive={activeTab === "4_settings"}
6372
iconComponent={<MdSettings className="text-2xl" />}
6473
/>
6574
</div>

web/components/search/search.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client";
2+
3+
import { usePathname, useSearchParams } from "next/navigation";
4+
import { MdOutlineSearch } from "react-icons/md";
5+
6+
type Props = { placeholder: string; setSearchString: (s: string) => void };
7+
export default function Search({ placeholder, setSearchString }: Props) {
8+
const searchParams = useSearchParams();
9+
const pathname = usePathname();
10+
11+
function handleSearch(term: string) {
12+
setSearchString(term);
13+
const params = new URLSearchParams(searchParams);
14+
if (term) {
15+
params.set("query", term);
16+
} else {
17+
params.delete("query");
18+
}
19+
const newUrl = `${pathname}?${params.toString()}`;
20+
history.replaceState(undefined, "", newUrl);
21+
}
22+
23+
return (
24+
<div className="relative mr-5 ml-5 flex flex-1 flex-shrink-0">
25+
<label htmlFor="search" className="sr-only">
26+
Search
27+
</label>
28+
<input
29+
className=" block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-none placeholder:text-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
30+
placeholder={placeholder}
31+
onChange={(e) => {
32+
handleSearch(e.target.value);
33+
}}
34+
defaultValue={searchParams.get("query")?.toString()}
35+
/>
36+
<MdOutlineSearch className="-translate-y-1/2 absolute top-1/2 left-3 h-[18px] w-[18px] text-gray-500 peer-focus:text-gray-900" />
37+
</div>
38+
);
39+
}

web/components/search/table.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
import { useMemo } from "react";
3+
import { useAll, useMyID } from "~/api/user";
4+
import { useModal } from "../common/modal/ModalProvider";
5+
import { HumanListItem } from "../human/humanListItem";
6+
7+
export default function UserTable({ query }: { query: string }) {
8+
const { openModal } = useModal();
9+
const {
10+
state: { data },
11+
} = useAll();
12+
const {
13+
state: { data: myId },
14+
} = useMyID();
15+
const initialData = useMemo(() => {
16+
return data?.filter((item) => item.id !== myId && item.id !== 0) ?? null;
17+
}, [data, myId]);
18+
const users = query
19+
? initialData?.filter((user) =>
20+
user.name.toLowerCase().includes(query.toLowerCase()),
21+
)
22+
: initialData;
23+
24+
return (
25+
<div>
26+
{users?.map((user) => (
27+
<HumanListItem
28+
key={user.id}
29+
id={user.id}
30+
name={user.name}
31+
pictureUrl={user.pictureUrl}
32+
onOpen={() => openModal(user)}
33+
/>
34+
))}
35+
</div>
36+
);
37+
}

0 commit comments

Comments
 (0)