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) => { @@ -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 ( + {alt} + ); +}; 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) => { {post.user.profileImage.alt} @@ -66,9 +69,12 @@ export const PostItem = ({ post }: Props) => { ) : null}

    - -

    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 ( -
    +
    { {post.user.profileImage.alt}
    @@ -75,9 +84,12 @@ export const TimelineItem = ({ post }: Props) => { @{post.user.username} - - -
    {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) => { - 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 - + +