diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000000..a06b9d7c6e
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,66 @@
+# WSH2026 作業指示
+
+## 採点方式
+- GitHub Actions で Lighthouse を実行
+- 満点: \
+1. [Lighthouse](https://github.com/GoogleChrome/lighthouse) を用いて、次の2つを検査します
+ - ページの表示(900点満点)
+ - ページの操作(250点満点)
+2. 検査したそれぞれのスコアを合算し、得点とします(1150点満点)
+
+## レギュレーション(絶対に守ること)
+- ## レギュレーション
+
+次に挙げる「明確に禁じていること」と「明確に参加者へ要求すること」を守れなかった場合、順位対象外になります。
+
+### 明確に許可されていること
+
+- **課題のレポジトリにあるコード・その他ファイルは、すべて変更してよい**
+ - API が返却する内容を変更して、新しい項目を追加したり既存の項目を削除したりしてよい
+ - ただし、運営が用意する fly.io 環境にデプロイする場合、fly.toml を変更してはならない
+- **外部のサービス(SaaS など)を自由に利用してよい**
+ - 無料で使えるサービスは https://free-for.dev/ などで調べられます
+ - 有料のサービスを使った場合の費用は自己負担となり、運営は負担しません
+
+### 明確に禁じていること
+
+- **Google Chrome 最新版において、著しい機能落ちやデザイン差異を発生させてはならない**
+ - 提供された VRT (Visual Regression Tests) が失敗しないこと
+ - [./test_cases.md](./test_cases.md) に記載された手動テスト項目が失敗しないこと
+- **シードに何らかの変更をしたとき、初期データのシードにある各種 ID を変更してはならない**
+- **明らかな悪意を持って VRT と手動テスト項目を通過させるためのコードを書いてはならない**
+- **競技終了後にデプロイしたアプリケーションを更新してはならない**
+- **`GET /api/v1/crok{?prompt}` のストリーミングプロトコル (Server-Sent Events) を変更してはならない**
+- **初期仕様の `crok-response.md` と同等の画面を構成するために必要な情報を Server-Sent Events 以外の方法で伝達してはならない**
+- **運営が用意する fly.io 環境にデプロイする場合、`fly.toml` の内容を変更してはならない**
+
+### 明確に参加者へ要求すること
+
+- **順位が確定するまでは、アプリケーションにアクセスできる状態であること**
+ - 競技終了後にレギュレーションチェックを行います
+ - レギュレーションチェックのときにアプリケーションにアクセスできない場合は、順位対象外になります
+- **API `POST /api/v1/initialize` にリクエストを送ると、データベースの内容が初期値にリセットされること**
+ - 採点サーバーは、データベースの内容が初期値であることを前提に計測をします
+ - 同等のデータを提供できるのであれば、機能落ちが発生しない範囲においてデータベースの内容は自由に書きかえて構いません
+
+- Google Chrome最新版で著しいUI差異・機能落ちを発生させない
+- VRTと手動テストを不正に通過させるコードを書かない
+
+## 変更のルール
+1. 変更は1施策ずつ /wsh-commit でコミットする
+2. UIに触る変更の後は必ずVRTが自動実行される
+3. ビルドが通らない状態でコミットしない
+
+## 優先施策(/wsh-analyze 完了後に記入)
+- [ ] 未記入
+
+## 禁止操作(レギュレーションを読んで追記)
+- **Google Chrome 最新版において、著しい機能落ちやデザイン差異を発生させてはならない**
+ - 提供された VRT (Visual Regression Tests) が失敗しないこと
+ - [./test_cases.md](./test_cases.md) に記載された手動テスト項目が失敗しないこと
+- **シードに何らかの変更をしたとき、初期データのシードにある各種 ID を変更してはならない**
+- **明らかな悪意を持って VRT と手動テスト項目を通過させるためのコードを書いてはならない**
+- **競技終了後にデプロイしたアプリケーションを更新してはならない**
+- **`GET /api/v1/crok{?prompt}` のストリーミングプロトコル (Server-Sent Events) を変更してはならない**
+- **初期仕様の `crok-response.md` と同等の画面を構成するために必要な情報を Server-Sent Events 以外の方法で伝達してはならない**
+- **運営が用意する fly.io 環境にデプロイする場合、`fly.toml` の内容を変更してはならない**
diff --git a/application/client/babel.config.js b/application/client/babel.config.js
index c3c574591a..7c6373189b 100644
--- a/application/client/babel.config.js
+++ b/application/client/babel.config.js
@@ -4,16 +4,16 @@ module.exports = {
[
"@babel/preset-env",
{
- targets: "ie 11",
+ targets: "last 2 Chrome versions",
corejs: "3",
- modules: "commonjs",
+ modules: false,
useBuiltIns: false,
},
],
[
"@babel/preset-react",
{
- development: true,
+ development: process.env.NODE_ENV !== "production",
runtime: "automatic",
},
],
diff --git a/application/client/package.json b/application/client/package.json
index 9f8e80a6a8..af2d3a5156 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": "NODE_ENV=production webpack",
"typecheck": "tsc"
},
"dependencies": {
diff --git a/application/client/src/components/application/SearchPage.tsx b/application/client/src/components/application/SearchPage.tsx
index e99045de45..9d9a97478b 100644
--- a/application/client/src/components/application/SearchPage.tsx
+++ b/application/client/src/components/application/SearchPage.tsx
@@ -1,6 +1,12 @@
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router";
-import { Field, InjectedFormProps, reduxForm, WrappedFieldProps } from "redux-form";
+import {
+ Field,
+ InjectedFormProps,
+ reduxForm,
+ SubmissionError,
+ WrappedFieldProps,
+} from "redux-form";
import { Timeline } from "@web-speed-hackathon-2026/client/src/components/timeline/Timeline";
import {
@@ -30,7 +36,7 @@ const SearchInput = ({ input, meta }: WrappedFieldProps) => (
placeholder="検索 (例: キーワード since:2025-01-01 until:2025-12-31)"
type="text"
/>
- {meta.touched && meta.error && (
+ {(meta.touched || meta.submitFailed) && meta.error && (
{meta.error}
)}
@@ -85,7 +91,12 @@ const SearchPageComponent = ({
}, [parsed]);
const onSubmit = (values: SearchFormData) => {
- const sanitizedText = sanitizeSearchText(values.searchText.trim());
+ const rawSearchText = values.searchText?.trim() ?? "";
+ if (rawSearchText.length === 0) {
+ throw new SubmissionError({ searchText: "検索キーワードを入力してください" });
+ }
+
+ const sanitizedText = sanitizeSearchText(rawSearchText);
navigate(`/search?q=${encodeURIComponent(sanitizedText)}`);
};
diff --git a/application/client/src/components/direct_message/DirectMessageListPage.tsx b/application/client/src/components/direct_message/DirectMessageListPage.tsx
index 5a373e918e..7169440ab1 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";
@@ -100,7 +99,15 @@ export const DirectMessageListPage = ({ activeUser, newDmModalId }: Props) => {
className="text-cax-text-subtle text-xs"
dateTime={lastMessage.createdAt}
>
- {moment(lastMessage.createdAt).locale("ja").fromNow()}
+ {(() => {
+ const diff = new Date(lastMessage.createdAt).getTime() - Date.now();
+ const rtf = new Intl.RelativeTimeFormat("ja", { numeric: "auto" });
+ const abs = Math.abs(diff);
+ if (abs < 60_000) return rtf.format(Math.round(diff / 1000), "second");
+ if (abs < 3_600_000) return rtf.format(Math.round(diff / 60_000), "minute");
+ if (abs < 86_400_000) return rtf.format(Math.round(diff / 3_600_000), "hour");
+ return rtf.format(Math.round(diff / 86_400_000), "day");
+ })()}
)}
diff --git a/application/client/src/components/direct_message/DirectMessagePage.tsx b/application/client/src/components/direct_message/DirectMessagePage.tsx
index 098c7d2894..da678b478c 100644
--- a/application/client/src/components/direct_message/DirectMessagePage.tsx
+++ b/application/client/src/components/direct_message/DirectMessagePage.tsx
@@ -1,10 +1,9 @@
import classNames from "classnames";
-import moment from "moment";
import {
ChangeEvent,
useCallback,
- useId,
useRef,
+ useId,
useState,
KeyboardEvent,
FormEvent,
@@ -34,7 +33,7 @@ export const DirectMessagePage = ({
onTyping,
onSubmit,
}: Props) => {
- const formRef = useRef