diff --git a/.gitignore b/.gitignore
index c9fdd6f0b0..1ec5fcd1a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -220,3 +220,4 @@ $RECYCLE.BIN/
application/upload/**
!application/upload/**/
!application/upload/**/.gitkeep
+cookie.txt
diff --git a/application/client/package.json b/application/client/package.json
index 9f8e80a6a8..43ad315c8f 100644
--- a/application/client/package.json
+++ b/application/client/package.json
@@ -5,7 +5,7 @@
"license": "MPL-2.0",
"author": "CyberAgent, Inc.",
"scripts": {
- "build": "NODE_ENV=development webpack",
+ "build": "webpack",
"typecheck": "tsc"
},
"dependencies": {
@@ -86,7 +86,9 @@
"typescript": "5.9.3",
"webpack": "5.102.1",
"webpack-cli": "6.0.1",
- "webpack-dev-server": "5.2.2"
+ "webpack-dev-server": "5.2.2",
+ "@tailwindcss/postcss": "^4.2.1",
+ "tailwindcss": "^4.2.1"
},
"engines": {
"node": "24.14.0"
diff --git a/application/client/postcss.config.js b/application/client/postcss.config.js
index d7ee920b94..0c9a35ec92 100644
--- a/application/client/postcss.config.js
+++ b/application/client/postcss.config.js
@@ -1,9 +1,11 @@
const postcssImport = require("postcss-import");
+const tailwindcss = require("@tailwindcss/postcss");
const postcssPresetEnv = require("postcss-preset-env");
module.exports = {
plugins: [
postcssImport(),
+ tailwindcss(),
postcssPresetEnv({
stage: 3,
}),
diff --git a/application/client/src/components/application/NavigationItem.tsx b/application/client/src/components/application/NavigationItem.tsx
index 57e64b004a..81d4425a5b 100644
--- a/application/client/src/components/application/NavigationItem.tsx
+++ b/application/client/src/components/application/NavigationItem.tsx
@@ -12,16 +12,24 @@ interface Props {
commandfor?: string;
}
-export const NavigationItem = ({ badge, href, icon, command, commandfor, text }: Props) => {
+export const NavigationItem = ({
+ badge,
+ href,
+ icon,
+ command,
+ commandfor,
+ text,
+}: Props) => {
const location = useLocation();
const isActive = location.pathname === href;
return (
{href !== undefined ? (
@@ -29,10 +37,13 @@ export const NavigationItem = ({ badge, href, icon, command, commandfor, text }:
{icon}
{badge}
- {text}
+
+ {text}
+
) : (
)}
diff --git a/application/client/src/components/direct_message/DirectMessageListPage.tsx b/application/client/src/components/direct_message/DirectMessageListPage.tsx
index 5a373e918e..235b970acf 100644
--- a/application/client/src/components/direct_message/DirectMessageListPage.tsx
+++ b/application/client/src/components/direct_message/DirectMessageListPage.tsx
@@ -1,4 +1,3 @@
-import moment from "moment";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@web-speed-hackathon-2026/client/src/components/foundation/Button";
@@ -13,6 +12,11 @@ interface Props {
newDmModalId: string;
}
+const jaDateTimeFormatter = new Intl.DateTimeFormat("ja-JP", {
+ dateStyle: "short",
+ timeStyle: "short",
+});
+
export const DirectMessageListPage = ({ activeUser, newDmModalId }: Props) => {
const [conversations, setConversations] =
useState | null>(null);
@@ -24,7 +28,8 @@ export const DirectMessageListPage = ({ activeUser, newDmModalId }: Props) => {
}
try {
- const conversations = await fetchJSON>("/api/v1/dm");
+ const conversations =
+ await fetchJSON>("/api/v1/dm");
setConversations(conversations);
setError(null);
} catch (error) {
@@ -53,7 +58,9 @@ export const DirectMessageListPage = ({ activeUser, newDmModalId }: Props) => {
}
+ leftItem={
+
+ }
>
新しくDMを始める
@@ -61,7 +68,9 @@ export const DirectMessageListPage = ({ activeUser, newDmModalId }: Props) => {
{error != null ? (
- DMの取得に失敗しました
+
+ DMの取得に失敗しました
+
) : conversations.length === 0 ? (
まだDMで会話した相手がいません。
@@ -82,7 +91,10 @@ export const DirectMessageListPage = ({ activeUser, newDmModalId }: Props) => {
return (
-
+
![{peer.profileImage.alt}]()
{
{peer.name}
-
@{peer.username}
+
+ @{peer.username}
+
{lastMessage != null && (
)}
-
{lastMessage?.body}
+
+ {lastMessage?.body}
+
{hasUnread ? (
未読
diff --git a/application/client/src/components/direct_message/DirectMessagePage.tsx b/application/client/src/components/direct_message/DirectMessagePage.tsx
index 098c7d2894..0fed217a8b 100644
--- a/application/client/src/components/direct_message/DirectMessagePage.tsx
+++ b/application/client/src/components/direct_message/DirectMessagePage.tsx
@@ -1,5 +1,4 @@
import classNames from "classnames";
-import moment from "moment";
import {
ChangeEvent,
useCallback,
@@ -25,6 +24,12 @@ interface Props {
onSubmit: (params: DirectMessageFormData) => Promise;
}
+const jaTimeFormatter = new Intl.DateTimeFormat("ja-JP", {
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+});
+
export const DirectMessagePage = ({
conversationError,
conversation,
@@ -38,7 +43,9 @@ export const DirectMessagePage = ({
const textAreaId = useId();
const peer =
- conversation.initiator.id !== activeUser.id ? conversation.initiator : conversation.member;
+ conversation.initiator.id !== activeUser.id
+ ? conversation.initiator
+ : conversation.member;
const [text, setText] = useState("");
const textAreaRows = Math.min((text || "").split("\n").length, 5);
@@ -55,7 +62,11 @@ export const DirectMessagePage = ({
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
- if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
+ if (
+ event.key === "Enter" &&
+ !event.shiftKey &&
+ !event.nativeEvent.isComposing
+ ) {
event.preventDefault();
formRef.current?.requestSubmit();
}
@@ -75,7 +86,9 @@ export const DirectMessagePage = ({
useEffect(() => {
const id = setInterval(() => {
- const height = Number(window.getComputedStyle(document.body).height.replace("px", ""));
+ const height = Number(
+ window.getComputedStyle(document.body).height.replace("px", ""),
+ );
if (height !== scrollHeightRef.current) {
scrollHeightRef.current = height;
window.scrollTo(0, height);
@@ -88,7 +101,9 @@ export const DirectMessagePage = ({
if (conversationError != null) {
return (
- メッセージの取得に失敗しました
+
+ メッセージの取得に失敗しました
+
);
}
@@ -141,7 +156,7 @@ export const DirectMessagePage = ({
{isActiveUserSend && message.isRead && (
既読
diff --git a/application/client/src/components/foundation/SimpleCoveredImage.tsx b/application/client/src/components/foundation/SimpleCoveredImage.tsx
new file mode 100644
index 0000000000..f91a474777
--- /dev/null
+++ b/application/client/src/components/foundation/SimpleCoveredImage.tsx
@@ -0,0 +1,15 @@
+interface Props {
+ src: string;
+ alt?: string;
+}
+
+export const SimpleCoveredImage = ({ src, alt = "" }: Props) => {
+ return (
+

+ );
+};
diff --git a/application/client/src/components/post/CommentItem.tsx b/application/client/src/components/post/CommentItem.tsx
index cb5bd38bda..9bcd64a961 100644
--- a/application/client/src/components/post/CommentItem.tsx
+++ b/application/client/src/components/post/CommentItem.tsx
@@ -1,5 +1,3 @@
-import moment from "moment";
-
import { Link } from "@web-speed-hackathon-2026/client/src/components/foundation/Link";
import { TranslatableText } from "@web-speed-hackathon-2026/client/src/components/post/TranslatableText";
import { getProfileImagePath } from "@web-speed-hackathon-2026/client/src/utils/get_path";
@@ -8,6 +6,10 @@ interface Props {
comment: Models.Comment;
}
+const jaLongDateFormatter = new Intl.DateTimeFormat("ja-JP", {
+ dateStyle: "long",
+});
+
export const CommentItem = ({ comment }: Props) => {
return (
@@ -42,8 +44,8 @@ export const CommentItem = ({ comment }: Props) => {
-
diff --git a/application/client/src/components/post/PostItem.tsx b/application/client/src/components/post/PostItem.tsx
index 5fa904c91a..86d741b5cc 100644
--- a/application/client/src/components/post/PostItem.tsx
+++ b/application/client/src/components/post/PostItem.tsx
@@ -1,5 +1,3 @@
-import moment from "moment";
-
import { Link } from "@web-speed-hackathon-2026/client/src/components/foundation/Link";
import { ImageArea } from "@web-speed-hackathon-2026/client/src/components/post/ImageArea";
import { MovieArea } from "@web-speed-hackathon-2026/client/src/components/post/MovieArea";
@@ -11,6 +9,10 @@ interface Props {
post: Models.Post;
}
+const jaLongDateFormatter = new Intl.DateTimeFormat("ja-JP", {
+ dateStyle: "long",
+});
+
export const PostItem = ({ post }: Props) => {
return (
@@ -24,6 +26,7 @@ export const PostItem = ({ post }: Props) => {
@@ -66,9 +69,12 @@ export const PostItem = ({ post }: Props) => {
) : null}
-
-
- {moment(post.createdAt).locale("ja").format("LL")}
+
+
+ {jaLongDateFormatter.format(new Date(post.createdAt))}
diff --git a/application/client/src/components/post/TimelineImageArea.tsx b/application/client/src/components/post/TimelineImageArea.tsx
new file mode 100644
index 0000000000..c8ab9aea0e
--- /dev/null
+++ b/application/client/src/components/post/TimelineImageArea.tsx
@@ -0,0 +1,34 @@
+import classNames from "classnames";
+
+import { SimpleCoveredImage } from "@web-speed-hackathon-2026/client/src/components/foundation/SimpleCoveredImage";
+import { getImagePath } from "@web-speed-hackathon-2026/client/src/utils/get_path";
+
+interface Props {
+ images: Models.Image[];
+}
+
+export const TimelineImageArea = ({ images }: Props) => {
+ return (
+
+
+ {images.map((image, idx) => {
+ return (
+
2 && (images.length !== 3 || idx !== 0),
+ "row-span-2":
+ images.length <= 2 || (images.length === 3 && idx === 0),
+ })}
+ >
+
+
+ );
+ })}
+
+
+ );
+};
diff --git a/application/client/src/components/post/TimelineMovieArea.tsx b/application/client/src/components/post/TimelineMovieArea.tsx
new file mode 100644
index 0000000000..6096fc6065
--- /dev/null
+++ b/application/client/src/components/post/TimelineMovieArea.tsx
@@ -0,0 +1,22 @@
+import { getMoviePath } from "@web-speed-hackathon-2026/client/src/utils/get_path";
+
+interface Props {
+ movie: Models.Movie;
+}
+
+export const TimelineMovieArea = ({ movie }: Props) => {
+ return (
+
+
})
+
+ );
+};
diff --git a/application/client/src/components/post/TimelineSoundArea.tsx b/application/client/src/components/post/TimelineSoundArea.tsx
new file mode 100644
index 0000000000..63f564da5e
--- /dev/null
+++ b/application/client/src/components/post/TimelineSoundArea.tsx
@@ -0,0 +1,23 @@
+interface Props {
+ sound: Models.Sound;
+}
+
+/**
+ * ホームタイムライン専用の軽量表示。
+ * 一覧ではメタ情報のみ表示し、重い波形描画や音声デコードを避ける。
+ */
+export const TimelineSoundArea = ({ sound }: Props) => {
+ return (
+
+
+ {sound.title}
+
+
+ {sound.artist}
+
+
+ );
+};
diff --git a/application/client/src/components/timeline/TimelineItem.tsx b/application/client/src/components/timeline/TimelineItem.tsx
index 21b88980f8..24663ec22c 100644
--- a/application/client/src/components/timeline/TimelineItem.tsx
+++ b/application/client/src/components/timeline/TimelineItem.tsx
@@ -1,14 +1,23 @@
-import moment from "moment";
import { MouseEventHandler, useCallback } from "react";
import { Link, useNavigate } from "react-router";
+import { TimelineImageArea } from "@web-speed-hackathon-2026/client/src/components/post/TimelineImageArea";
+import { TimelineMovieArea } from "@web-speed-hackathon-2026/client/src/components/post/TimelineMovieArea";
+import { TimelineSoundArea } from "@web-speed-hackathon-2026/client/src/components/post/TimelineSoundArea";
import { ImageArea } from "@web-speed-hackathon-2026/client/src/components/post/ImageArea";
import { MovieArea } from "@web-speed-hackathon-2026/client/src/components/post/MovieArea";
import { SoundArea } from "@web-speed-hackathon-2026/client/src/components/post/SoundArea";
import { TranslatableText } from "@web-speed-hackathon-2026/client/src/components/post/TranslatableText";
import { getProfileImagePath } from "@web-speed-hackathon-2026/client/src/utils/get_path";
-const isClickedAnchorOrButton = (target: EventTarget | null, currentTarget: Element): boolean => {
+const jaLongDateFormatter = new Intl.DateTimeFormat("ja-JP", {
+ dateStyle: "long",
+});
+
+const isClickedAnchorOrButton = (
+ target: EventTarget | null,
+ currentTarget: Element,
+): boolean => {
while (target !== null && target instanceof Element) {
const tagName = target.tagName.toLowerCase();
if (["button", "a"].includes(tagName)) {
@@ -22,10 +31,6 @@ const isClickedAnchorOrButton = (target: EventTarget | null, currentTarget: Elem
return false;
};
-/**
- * @typedef {object} Props
- * @property {Models.Post} post
- */
interface Props {
post: Models.Post;
}
@@ -33,13 +38,13 @@ interface Props {
export const TimelineItem = ({ post }: Props) => {
const navigate = useNavigate();
- /**
- * ボタンやリンク以外の箇所をクリックしたとき かつ 文字が選択されてないとき、投稿詳細ページに遷移する
- */
const handleClick = useCallback(
(ev) => {
const isSelectedText = document.getSelection()?.isCollapsed === false;
- if (!isClickedAnchorOrButton(ev.target, ev.currentTarget) && !isSelectedText) {
+ if (
+ !isClickedAnchorOrButton(ev.target, ev.currentTarget) &&
+ !isSelectedText
+ ) {
navigate(`/posts/${post.id}`);
}
},
@@ -47,7 +52,10 @@ export const TimelineItem = ({ post }: Props) => {
);
return (
-
+
{
@@ -75,9 +84,12 @@ export const TimelineItem = ({ post }: Props) => {
@{post.user.username}
-
-
-
- {moment(post.createdAt).locale("ja").format("LL")}
+
+
+ {jaLongDateFormatter.format(new Date(post.createdAt))}
@@ -85,6 +97,21 @@ export const TimelineItem = ({ post }: Props) => {
{post.images?.length > 0 ? (
+
+
+
+ ) : null}
+ {post.movie ? (
+
+
+
+ ) : null}
+ {post.sound ? (
+
+
+
+ ) : null}
+ {/* {post.images?.length > 0 ? (
@@ -98,7 +125,7 @@ export const TimelineItem = ({ post }: Props) => {
- ) : null}
+ ) : null} */}
diff --git a/application/client/src/components/user_profile/UserProfileHeader.tsx b/application/client/src/components/user_profile/UserProfileHeader.tsx
index c1c3355e19..9107d72a5c 100644
--- a/application/client/src/components/user_profile/UserProfileHeader.tsx
+++ b/application/client/src/components/user_profile/UserProfileHeader.tsx
@@ -1,5 +1,4 @@
import { FastAverageColor } from "fast-average-color";
-import moment from "moment";
import { ReactEventHandler, useCallback, useState } from "react";
import { FontAwesomeIcon } from "@web-speed-hackathon-2026/client/src/components/foundation/FontAwesomeIcon";
@@ -9,17 +8,24 @@ interface Props {
user: Models.User;
}
+const jaLongDateFormatter = new Intl.DateTimeFormat("ja-JP", {
+ dateStyle: "long",
+});
+
export const UserProfileHeader = ({ user }: Props) => {
const [averageColor, setAverageColor] = useState(null);
// 画像の平均色を取得します
/** @type {React.ReactEventHandler} */
- const handleLoadImage = useCallback>((ev) => {
- const fac = new FastAverageColor();
- const { rgb } = fac.getColor(ev.currentTarget, { mode: "precision" });
- setAverageColor(rgb);
- fac.destroy();
- }, []);
+ const handleLoadImage = useCallback>(
+ (ev) => {
+ const fac = new FastAverageColor();
+ const { rgb } = fac.getColor(ev.currentTarget, { mode: "precision" });
+ setAverageColor(rgb);
+ fac.destroy();
+ },
+ [],
+ );
return (
@@ -43,8 +49,8 @@ export const UserProfileHeader = ({ user }: Props) => {
-
- {moment(user.createdAt).locale("ja").format("LL")}
+
+ {jaLongDateFormatter.format(new Date(user.createdAt))}
からサービスを利用しています
diff --git a/application/client/src/containers/AppContainer.tsx b/application/client/src/containers/AppContainer.tsx
index d66858a949..cdd0c73cbe 100644
--- a/application/client/src/containers/AppContainer.tsx
+++ b/application/client/src/containers/AppContainer.tsx
@@ -1,10 +1,18 @@
-import { useCallback, useEffect, useId, useState } from "react";
+import {
+ lazy,
+ Suspense,
+ useCallback,
+ useEffect,
+ useId,
+ useRef,
+ useState,
+} from "react";
import { Helmet, HelmetProvider } from "react-helmet";
import { Route, Routes, useLocation, useNavigate } from "react-router";
import { AppPage } from "@web-speed-hackathon-2026/client/src/components/application/AppPage";
import { AuthModalContainer } from "@web-speed-hackathon-2026/client/src/containers/AuthModalContainer";
-import { CrokContainer } from "@web-speed-hackathon-2026/client/src/containers/CrokContainer";
+// import { CrokContainer } from "@web-speed-hackathon-2026/client/src/containers/CrokContainer";
import { DirectMessageContainer } from "@web-speed-hackathon-2026/client/src/containers/DirectMessageContainer";
import { DirectMessageListContainer } from "@web-speed-hackathon-2026/client/src/containers/DirectMessageListContainer";
import { NewPostModalContainer } from "@web-speed-hackathon-2026/client/src/containers/NewPostModalContainer";
@@ -14,26 +22,51 @@ import { SearchContainer } from "@web-speed-hackathon-2026/client/src/containers
import { TermContainer } from "@web-speed-hackathon-2026/client/src/containers/TermContainer";
import { TimelineContainer } from "@web-speed-hackathon-2026/client/src/containers/TimelineContainer";
import { UserProfileContainer } from "@web-speed-hackathon-2026/client/src/containers/UserProfileContainer";
-import { fetchJSON, sendJSON } from "@web-speed-hackathon-2026/client/src/utils/fetchers";
+import {
+ fetchJSON,
+ sendJSON,
+} from "@web-speed-hackathon-2026/client/src/utils/fetchers";
+
+const CrokContainer = lazy(() =>
+ import("@web-speed-hackathon-2026/client/src/containers/CrokContainer").then(
+ (m) => ({ default: m.CrokContainer }),
+ ),
+);
export const AppContainer = () => {
const { pathname } = useLocation();
const navigate = useNavigate();
+
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
const [activeUser, setActiveUser] = useState(null);
- const [isLoadingActiveUser, setIsLoadingActiveUser] = useState(true);
+ const activeUserRequestIdRef = useRef(0);
+
+ const reloadActiveUser = useCallback(async () => {
+ const requestId = ++activeUserRequestIdRef.current;
+
+ try {
+ const user = await fetchJSON("/api/v1/me");
+ if (requestId !== activeUserRequestIdRef.current) {
+ return null;
+ }
+ setActiveUser(user);
+ return user;
+ } catch {
+ if (requestId !== activeUserRequestIdRef.current) {
+ return null;
+ }
+ setActiveUser(null);
+ return null;
+ }
+ }, []);
+
useEffect(() => {
- void fetchJSON("/api/v1/me")
- .then((user) => {
- setActiveUser(user);
- })
- .finally(() => {
- setIsLoadingActiveUser(false);
- });
- }, [setActiveUser, setIsLoadingActiveUser]);
+ void reloadActiveUser();
+ }, [reloadActiveUser]);
+
const handleLogout = useCallback(async () => {
await sendJSON("/api/v1/signout", {});
setActiveUser(null);
@@ -43,16 +76,6 @@ export const AppContainer = () => {
const authModalId = useId();
const newPostModalId = useId();
- if (isLoadingActiveUser) {
- return (
-
-
- 読込中 - CaX
-
-
- );
- }
-
return (
{
newPostModalId={newPostModalId}
onLogout={handleLogout}
>
-
- } path="/" />
-
- }
- path="/dm"
- />
- }
- path="/dm/:conversationId"
- />
- } path="/search" />
- } path="/users/:username" />
- } path="/posts/:postId" />
- } path="/terms" />
- }
- path="/crok"
- />
- } path="*" />
-
+
+
+ } path="/" />
+
+ }
+ path="/dm"
+ />
+
+ }
+ path="/dm/:conversationId"
+ />
+ } path="/search" />
+ } path="/users/:username" />
+ } path="/posts/:postId" />
+ } path="/terms" />
+
+ }
+ path="/crok"
+ />
+ } path="*" />
+
+
diff --git a/application/client/src/containers/AuthModalContainer.tsx b/application/client/src/containers/AuthModalContainer.tsx
index 8d159f3528..13c255d5a5 100644
--- a/application/client/src/containers/AuthModalContainer.tsx
+++ b/application/client/src/containers/AuthModalContainer.tsx
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
+import { flushSync } from "react-dom";
import { SubmissionError } from "redux-form";
import { AuthFormData } from "@web-speed-hackathon-2026/client/src/auth/types";
@@ -16,7 +17,10 @@ const ERROR_MESSAGES: Record = {
USERNAME_TAKEN: "ユーザー名が使われています",
};
-function getErrorCode(err: JQuery.jqXHR, type: "signin" | "signup"): string {
+function getErrorCode(
+ err: JQuery.jqXHR,
+ type: "signin" | "signup"
+): string {
const responseJSON = err.responseJSON;
if (
typeof responseJSON !== "object" ||
@@ -38,34 +42,41 @@ function getErrorCode(err: JQuery.jqXHR, type: "signin" | "signup"): st
export const AuthModalContainer = ({ id, onUpdateActiveUser }: Props) => {
const ref = useRef(null);
const [resetKey, setResetKey] = useState(0);
+
useEffect(() => {
if (!ref.current) return;
const element = ref.current;
const handleToggle = () => {
- // モーダル開閉時にkeyを更新することでフォームの状態をリセットする
setResetKey((key) => key + 1);
};
element.addEventListener("toggle", handleToggle);
return () => {
element.removeEventListener("toggle", handleToggle);
};
- }, [ref, setResetKey]);
+ }, []);
const handleRequestCloseModal = useCallback(() => {
ref.current?.close();
- }, [ref]);
-
+ }, []);
const handleSubmit = useCallback(
async (values: AuthFormData) => {
try {
+ let user: Models.User;
if (values.type === "signup") {
- const user = await sendJSON("/api/v1/signup", values);
- onUpdateActiveUser(user);
+ user = await sendJSON("/api/v1/signup", values);
} else {
- const user = await sendJSON("/api/v1/signin", values);
- onUpdateActiveUser(user);
+ user = await sendJSON("/api/v1/signin", values);
}
+
+ flushSync(() => {
+ onUpdateActiveUser(user);
+ });
+
+ await new Promise((resolve) => {
+ requestAnimationFrame(() => resolve());
+ });
+
handleRequestCloseModal();
} catch (err: unknown) {
const error = getErrorCode(err as JQuery.jqXHR, values.type);
@@ -74,7 +85,7 @@ export const AuthModalContainer = ({ id, onUpdateActiveUser }: Props) => {
});
}
},
- [handleRequestCloseModal, onUpdateActiveUser],
+ [handleRequestCloseModal, onUpdateActiveUser]
);
return (
diff --git a/application/client/src/hooks/use_infinite_fetch.ts b/application/client/src/hooks/use_infinite_fetch.ts
index 394fccd9ea..63ee4c9050 100644
--- a/application/client/src/hooks/use_infinite_fetch.ts
+++ b/application/client/src/hooks/use_infinite_fetch.ts
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
-const LIMIT = 30;
+const LIMIT = 10;
interface ReturnValues {
data: Array;
@@ -11,9 +11,9 @@ interface ReturnValues {
export function useInfiniteFetch(
apiPath: string,
- fetcher: (apiPath: string) => Promise,
+ fetcher: (apiPath: string) => Promise
): ReturnValues {
- const internalRef = useRef({ isLoading: false, offset: 0 });
+ const internalRef = useRef({ isLoading: false, offset: 0, hasMore: true });
const [result, setResult] = useState, "fetchMore">>({
data: [],
@@ -22,8 +22,8 @@ export function useInfiniteFetch(
});
const fetchMore = useCallback(() => {
- const { isLoading, offset } = internalRef.current;
- if (isLoading) {
+ const { isLoading, offset, hasMore } = internalRef.current;
+ if (isLoading || !hasMore) {
return;
}
@@ -32,20 +32,24 @@ export function useInfiniteFetch(
isLoading: true,
}));
internalRef.current = {
+ ...internalRef.current,
isLoading: true,
- offset,
};
- void fetcher(apiPath).then(
- (allData) => {
+ const separator = apiPath.includes("?") ? "&" : "?";
+ const pagedApiPath = `${apiPath}${separator}limit=${LIMIT}&offset=${offset}`;
+
+ void fetcher(pagedApiPath).then(
+ (pagedData) => {
setResult((cur) => ({
...cur,
- data: [...cur.data, ...allData.slice(offset, offset + LIMIT)],
+ data: [...cur.data, ...pagedData],
isLoading: false,
}));
internalRef.current = {
isLoading: false,
- offset: offset + LIMIT,
+ offset: offset + pagedData.length,
+ hasMore: pagedData.length === LIMIT,
};
},
(error) => {
@@ -55,22 +59,23 @@ export function useInfiniteFetch(
isLoading: false,
}));
internalRef.current = {
+ ...internalRef.current,
isLoading: false,
- offset,
};
- },
+ }
);
}, [apiPath, fetcher]);
useEffect(() => {
- setResult(() => ({
+ setResult({
data: [],
error: null,
isLoading: true,
- }));
+ });
internalRef.current = {
isLoading: false,
offset: 0,
+ hasMore: true,
};
fetchMore();
diff --git a/application/client/src/index.css b/application/client/src/index.css
index 8612ebcdd2..9222352f47 100644
--- a/application/client/src/index.css
+++ b/application/client/src/index.css
@@ -5,7 +5,7 @@
@font-face {
/* Source Han Serif JP Regular の Y 軸を 1/1.43 に縮小した改変フォント */
font-family: "Rei no Are Mincho";
- font-display: block;
+ font-display: swap;
src: url(/fonts/ReiNoAreMincho-Regular.otf) format("opentype");
font-weight: normal;
}
@@ -13,7 +13,7 @@
@font-face {
/* Source Han Serif JP Heavy の Y 軸を 1/1.43 に縮小した改変フォント */
font-family: "Rei no Are Mincho";
- font-display: block;
+ font-display: swap;
src: url(/fonts/ReiNoAreMincho-Heavy.otf) format("opentype");
font-weight: bold;
}
diff --git a/application/client/src/index.html b/application/client/src/index.html
index 3d949e7473..d03ebe3bef 100644
--- a/application/client/src/index.html
+++ b/application/client/src/index.html
@@ -4,8 +4,15 @@
CaX
-
+
+