Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
Loading