Skip to content

Commit

Permalink
ユーザー名検索 (#524)
Browse files Browse the repository at this point in the history
# 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]>
  • Loading branch information
3 people authored Dec 3, 2024
1 parent 84a13ab commit 4c60edc
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 16 deletions.
12 changes: 11 additions & 1 deletion web/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import type { Hook as UseHook } from "./share/types.ts";

const UserListSchema = z.array(UserSchema);

export function useAll(): Hook<User[]> {
return useCustomizedSWR("users::all", all, UserListSchema);
}
export function useRecommended(): UseHook<User[]> {
const url = endpoints.recommendedUsers;
return useAuthorizedData<User[]>(url);
Expand All @@ -28,6 +31,11 @@ export function usePendingFromMe(): Hook<User[]> {
);
}

async function all(): Promise<User[]> {
const res = await credFetch("GET", endpoints.users);
return res.json();
}

async function matched(): Promise<User[]> {
const res = await credFetch("GET", endpoints.matchedUsers);
return res.json();
Expand Down Expand Up @@ -131,6 +139,8 @@ export async function deleteAccount(): Promise<void> {
const res = await credFetch("DELETE", endpoints.me);
if (res.status !== 204)
throw new Error(
`failed to delete account: expected status code 204, but got ${res.status} with text ${await res.text()}`,
`failed to delete account: expected status code 204, but got ${
res.status
} with text ${await res.text()}`,
);
}
2 changes: 1 addition & 1 deletion web/app/chat/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function ChatPageLayout({
<div className="absolute top-14 right-0 bottom-14 left-0 overflow-y-auto sm:top-16">
{children}
</div>
<BottomBar activeTab="2_chat" />
<BottomBar activeTab="3_chat" />
</NavigateByAuthState>
);
}
5 changes: 3 additions & 2 deletions web/app/friends/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import BottomBar from "~/components/BottomBar";
import Header from "~/components/Header";
import { NavigateByAuthState } from "~/components/common/NavigateByAuthState";

export default function FriendsPageLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<NavigateByAuthState type="toLoginForUnauthenticated">
<Header title="フレンド/Friends" />
<div className="absolute top-14 right-0 bottom-14 left-0 overflow-y-auto sm:top-16">
{children}
</div>
<BottomBar activeTab="1_friends" />
</>
</NavigateByAuthState>
);
}
5 changes: 3 additions & 2 deletions web/app/home/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import BottomBar from "~/components/BottomBar";
import Header from "~/components/Header";
import { NavigateByAuthState } from "~/components/common/NavigateByAuthState";

export default function HomePageLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<NavigateByAuthState type="toLoginForUnauthenticated">
<Header title="ホーム/Home" />
<div className="relative top-14 right-0 bottom-14 left-0 overflow-y-auto sm:top-16">
{children}
</div>
<BottomBar activeTab="0_home" />
</>
</NavigateByAuthState>
);
}
18 changes: 18 additions & 0 deletions web/app/search/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import BottomBar from "~/components/BottomBar";
import Header from "~/components/Header";

export default function HomePageLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<Header title="検索/Search" />
<div className="absolute top-14 right-0 bottom-14 left-0 overflow-y-auto sm:top-16">
{children}
</div>
<BottomBar activeTab="2_search" />
</>
);
}
27 changes: 27 additions & 0 deletions web/app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import type React from "react";
import { useState } from "react";
import Search from "~/components/search/search";
import Table from "~/components/search/table";

export default function SearchPage({
searchParams,
}: {
searchParams?: {
query?: string;
page?: string;
};
}) {
const [query, setQuery] = useState<string>(searchParams?.query ?? "");

return (
<div className="flex min-h-screen justify-center ">
<div className="w-full">
<h2 className="m-5 mb-4 font-bold text-2xl">ユーザー検索</h2>
<Search placeholder="検索" setSearchString={setQuery} />
<Table query={query} />
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion web/app/settings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function SettingsPageLayout({
<div className="absolute top-14 right-0 bottom-14 left-0 overflow-y-auto sm:top-16">
{children}
</div>
<BottomBar activeTab="3_settings" />
<BottomBar activeTab="4_settings" />
</>
);
}
17 changes: 13 additions & 4 deletions web/components/BottomBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { MdHome } from "react-icons/md";
import { MdPeople } from "react-icons/md";
import { MdChat } from "react-icons/md";
import { MdSettings } from "react-icons/md";
import { MdSearch } from "react-icons/md";

type Props = {
activeTab: "0_home" | "1_friends" | "2_chat" | "3_settings";
activeTab: "0_home" | "1_friends" | "2_search" | "3_chat" | "4_settings";
};

function BottomBarCell({
Expand All @@ -22,7 +23,9 @@ function BottomBarCell({
return (
<Link
href={href}
className={`focus:bg-gray-300 ${isActive ? "active text-primary" : "text-secondary"}`}
className={`focus:bg-gray-300 ${
isActive ? "active text-primary" : "text-secondary"
}`}
>
{iconComponent}
<span
Expand Down Expand Up @@ -50,16 +53,22 @@ export default function BottomBar(props: Props) {
isActive={activeTab === "1_friends"}
iconComponent={<MdPeople className="text-2xl" />}
/>
<BottomBarCell
label="Search"
href="/search"
isActive={activeTab === "2_search"}
iconComponent={<MdSearch className="text-2xl" />}
/>
<BottomBarCell
label="Chat"
href="/chat"
isActive={activeTab === "2_chat"}
isActive={activeTab === "3_chat"}
iconComponent={<MdChat className="text-2xl" />}
/>
<BottomBarCell
label="Settings"
href="/settings"
isActive={activeTab === "3_settings"}
isActive={activeTab === "4_settings"}
iconComponent={<MdSettings className="text-2xl" />}
/>
</div>
Expand Down
39 changes: 39 additions & 0 deletions web/components/search/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { usePathname, useSearchParams } from "next/navigation";
import { MdOutlineSearch } from "react-icons/md";

type Props = { placeholder: string; setSearchString: (s: string) => void };
export default function Search({ placeholder, setSearchString }: Props) {
const searchParams = useSearchParams();
const pathname = usePathname();

function handleSearch(term: string) {
setSearchString(term);
const params = new URLSearchParams(searchParams);
if (term) {
params.set("query", term);
} else {
params.delete("query");
}
const newUrl = `${pathname}?${params.toString()}`;
history.replaceState(undefined, "", newUrl);
}

return (
<div className="relative mr-5 ml-5 flex flex-1 flex-shrink-0">
<label htmlFor="search" className="sr-only">
Search
</label>
<input
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"
placeholder={placeholder}
onChange={(e) => {
handleSearch(e.target.value);
}}
defaultValue={searchParams.get("query")?.toString()}
/>
<MdOutlineSearch className="-translate-y-1/2 absolute top-1/2 left-3 h-[18px] w-[18px] text-gray-500 peer-focus:text-gray-900" />
</div>
);
}
37 changes: 37 additions & 0 deletions web/components/search/table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";
import { useMemo } from "react";
import { useAll, useMyID } from "~/api/user";
import { useModal } from "../common/modal/ModalProvider";
import { HumanListItem } from "../human/humanListItem";

export default function UserTable({ query }: { query: string }) {
const { openModal } = useModal();
const {
state: { data },
} = useAll();
const {
state: { data: myId },
} = useMyID();
const initialData = useMemo(() => {
return data?.filter((item) => item.id !== myId && item.id !== 0) ?? null;
}, [data, myId]);
const users = query
? initialData?.filter((user) =>
user.name.toLowerCase().includes(query.toLowerCase()),
)
: initialData;

return (
<div>
{users?.map((user) => (
<HumanListItem
key={user.id}
id={user.id}
name={user.name}
pictureUrl={user.pictureUrl}
onOpen={() => openModal(user)}
/>
))}
</div>
);
}
17 changes: 12 additions & 5 deletions web/hooks/useCustomizedSWR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,17 @@ export function useCustomizedSWR<T>(
): Hook<T> {
const CACHE_KEY = SWR_PREFIX + cacheKey;

const [state, setState] = useState<State<T>>(() =>
loadOldData(CACHE_KEY, schema),
);
// HACK: 最初の描画時の挙動をサーバー上の挙動とそろえるため、 useEffect() でstaleデータの読み込みを遅延している。
// >> https://github.com/vercel/next.js/discussions/17443
// >> Code that is only supposed to run in the browser should be executed inside useEffect. That's required because the first render should match the initial render of the server. If you manipulate that result it creates a mismatch and React won't be able to hydrate the page successfully.
const [state, setState] = useState<State<T>>({
current: "loading",
data: null,
error: null,
});
useEffect(() => {
setState(loadOldData(CACHE_KEY, schema));
}, [CACHE_KEY, schema]);

const reload = useCallback(async () => {
setState((state) =>
Expand All @@ -77,7 +85,6 @@ export function useCustomizedSWR<T>(
console.error(
`useSWR: Schema Parse Error | in incoming data | at schema ${CACHE_KEY} | Error: ${result.error.message}`,
);
console.log("data:", data);
}
setState({
data: data,
Expand Down Expand Up @@ -152,5 +159,5 @@ function loadOldData<T>(
}

function go(fn: () => Promise<void>) {
fn();
fn().catch(console.warn);
}

0 comments on commit 4c60edc

Please sign in to comment.