Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
513942b
Optimize client performance and stabilize DM pages
Atlas-45 Mar 20, 2026
7af28d1
Reduce client work and fix search flow
Atlas-45 Mar 20, 2026
85433df
Gate DM websocket and read calls
Atlas-45 Mar 20, 2026
8dc6dbf
Trim payloads and add image loading hints
Atlas-45 Mar 20, 2026
0077a13
Defer media work and preserve uploaded image alt
Atlas-45 Mar 20, 2026
8f9df3d
Delay auth and audio work on landing
Atlas-45 Mar 20, 2026
bff5860
Prioritize first-view content and trim DM payloads
Atlas-45 Mar 20, 2026
297585d
Prevent stale auth fetch from overwriting sign-in
Atlas-45 Mar 20, 2026
4c4ed87
Bootstrap critical data for key pages
Atlas-45 Mar 20, 2026
d6d300d
Fix Express 5 SPA fallback route
Atlas-45 Mar 21, 2026
6e44f98
Lighten auth flow and Crok rendering
Atlas-45 Mar 21, 2026
d6d63e3
Prerender critical shells and simplify auth flow
Atlas-45 Mar 21, 2026
256b848
Serve prerendered shell for SPA routes
Atlas-45 Mar 21, 2026
96ba6ed
Hydrate client state from prerendered bootstrap
Atlas-45 Mar 21, 2026
2e17134
Keep prerendered shell visible until app mount
Atlas-45 Mar 21, 2026
27fc4a1
Enable keep-alive and static asset caching
Atlas-45 Mar 21, 2026
15b0427
Defer prerender loader import from server startup
Atlas-45 Mar 21, 2026
ac1c3e4
Align home bootstrap payload with client page size
Atlas-45 Mar 21, 2026
0e40a2c
Compress responses and stabilize DM sends
Atlas-45 Mar 21, 2026
b160822
Reduce auth flow work and shrink prerender shell
Atlas-45 Mar 21, 2026
d0abbe5
Prevent DM reload races after optimistic sends
Atlas-45 Mar 21, 2026
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
7 changes: 3 additions & 4 deletions application/client/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@ module.exports = {
[
"@babel/preset-env",
{
targets: "ie 11",
corejs: "3",
modules: "commonjs",
targets: "last 1 Chrome version",
modules: false,
useBuiltIns: false,
},
],
[
"@babel/preset-react",
{
development: true,
development: process.env.NODE_ENV !== "production",
runtime: "automatic",
},
],
Expand Down
4 changes: 3 additions & 1 deletion application/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -57,6 +57,7 @@
"@babel/preset-env": "7.28.3",
"@babel/preset-react": "7.27.1",
"@babel/preset-typescript": "7.27.1",
"@tailwindcss/postcss": "4.2.2",
"@tsconfig/strictest": "2.0.8",
"@types/bluebird": "3.5.42",
"@types/common-tags": "1.8.4",
Expand All @@ -83,6 +84,7 @@
"postcss-loader": "8.2.0",
"postcss-preset-env": "10.4.0",
"react-markdown": "10.1.0",
"tailwindcss": "4.2.2",
"typescript": "5.9.3",
"webpack": "5.102.1",
"webpack-cli": "6.0.1",
Expand Down
2 changes: 2 additions & 0 deletions application/client/postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const tailwindcss = require("@tailwindcss/postcss");
const postcssImport = require("postcss-import");
const postcssPresetEnv = require("postcss-preset-env");

module.exports = {
plugins: [
postcssImport(),
tailwindcss(),
postcssPresetEnv({
stage: 3,
}),
Expand Down
8 changes: 4 additions & 4 deletions application/client/src/auth/validation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { FormErrors } from "redux-form";

import { AuthFormData } from "@web-speed-hackathon-2026/client/src/auth/types";

export const validate = (values: AuthFormData): FormErrors<AuthFormData> => {
const errors: FormErrors<AuthFormData> = {};
export type AuthFormErrors = Partial<Record<keyof AuthFormData, string>>;

export const validate = (values: AuthFormData): AuthFormErrors => {
const errors: AuthFormErrors = {};

const normalizedName = values.name?.trim() || "";
const normalizedPassword = values.password?.trim() || "";
Expand Down
1 change: 1 addition & 0 deletions application/client/src/buildinfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ declare global {
BUILD_DATE: string | undefined;
COMMIT_HASH: string | undefined;
};
var __BOOTSTRAP_DATA__: Record<string, unknown> | undefined;
}

/** @note 競技用サーバーで参照します。可能な限りコード内に含めてください */
Expand Down
3 changes: 3 additions & 0 deletions application/client/src/components/application/AccountMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export const AccountMenu = ({ user, onLogout }: Props) => {
<img
alt={user.profileImage.alt}
className="h-10 w-10 shrink-0 rounded-full object-cover"
decoding="async"
height="40"
src={getProfileImagePath(user.profileImage.id)}
width="40"
/>
<div className="hidden min-w-0 flex-1 text-left lg:block">
<div className="text-cax-text truncate text-sm font-bold">{user.name}</div>
Expand Down
38 changes: 25 additions & 13 deletions application/client/src/components/application/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from "@web-speed-hackathon-2026/client/src/search/services";
import { SearchFormData } from "@web-speed-hackathon-2026/client/src/search/types";
import { validate } from "@web-speed-hackathon-2026/client/src/search/validation";
import { analyzeSentiment } from "@web-speed-hackathon-2026/client/src/utils/negaposi_analyzer";

import { Button } from "../foundation/Button";

Expand All @@ -22,6 +21,7 @@ const SearchInput = ({ input, meta }: WrappedFieldProps) => (
<div className="flex flex-1 flex-col">
<input
{...input}
aria-label="検索 (例: キーワード since:2025-01-01 until:2025-12-31)"
className={`flex-1 rounded border px-4 py-2 focus:outline-none ${
meta.touched && meta.error
? "border-cax-danger focus:border-cax-danger"
Expand Down Expand Up @@ -53,20 +53,32 @@ const SearchPageComponent = ({
}

let isMounted = true;
analyzeSentiment(parsed.keywords)
.then((result) => {
if (isMounted) {
setIsNegative(result.label === "negative");
}
})
.catch(() => {
if (isMounted) {
setIsNegative(false);
}
});
const run = () => {
void import("@web-speed-hackathon-2026/client/src/utils/negaposi_analyzer")
.then(({ analyzeSentiment }) => analyzeSentiment(parsed.keywords))
.then((result) => {
if (isMounted) {
setIsNegative(result.label === "negative");
}
})
.catch(() => {
if (isMounted) {
setIsNegative(false);
}
});
};

const timerId = window.setTimeout(() => {
if ("requestIdleCallback" in window) {
window.requestIdleCallback(run, { timeout: 5000 });
} else {
run();
}
}, 2000);

return () => {
isMounted = false;
window.clearTimeout(timerId);
};
}, [parsed.keywords]);

Expand All @@ -82,7 +94,7 @@ const SearchPageComponent = ({
parts.push(`${parsed.untilDate} 以前`);
}
return parts.join(" ");
}, [parsed]);
}, [parsed.keywords, parsed.sinceDate, parsed.untilDate]);

const onSubmit = (values: SearchFormData) => {
const sanitizedText = sanitizeSearchText(values.searchText.trim());
Expand Down
172 changes: 108 additions & 64 deletions application/client/src/components/auth_modal/AuthModalPage.tsx
Original file line number Diff line number Diff line change
@@ -1,82 +1,134 @@
import { useSelector } from "react-redux";
import { Field, formValueSelector, InjectedFormProps, reduxForm } from "redux-form";
import { ChangeEvent, FormEvent, useCallback, useMemo, useState } from "react";

import { AuthFormData } from "@web-speed-hackathon-2026/client/src/auth/types";
import { validate } from "@web-speed-hackathon-2026/client/src/auth/validation";
import { FormInputField } from "@web-speed-hackathon-2026/client/src/components/foundation/FormInputField";
import { Input } from "@web-speed-hackathon-2026/client/src/components/foundation/Input";
import { Link } from "@web-speed-hackathon-2026/client/src/components/foundation/Link";
import { ModalErrorMessage } from "@web-speed-hackathon-2026/client/src/components/modal/ModalErrorMessage";
import { ModalSubmitButton } from "@web-speed-hackathon-2026/client/src/components/modal/ModalSubmitButton";

interface Props {
onRequestCloseModal: () => void;
onSubmit: (values: AuthFormData) => Promise<string | null>;
}

const AuthModalPageComponent = ({
onRequestCloseModal,
handleSubmit,
error,
invalid,
submitting,
initialValues,
change,
}: Props & InjectedFormProps<AuthFormData, Props>) => {
const currentType: "signin" | "signup" = useSelector((state) =>
// @ts-ignore: formValueSelectorの型付けが弱いため、型に嘘をつく
formValueSelector("auth")(state, "type"),
const INITIAL_VALUES: AuthFormData = {
type: "signin",
username: "",
name: "",
password: "",
};

export const AuthModalPage = ({ onRequestCloseModal, onSubmit }: Props) => {
const [values, setValues] = useState<AuthFormData>(INITIAL_VALUES);
const [submitError, setSubmitError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);

const errors = useMemo(() => validate(values), [values]);
const isInvalid = Object.values(errors).some(Boolean);

const handleChange = useCallback(
(key: keyof AuthFormData) => (event: ChangeEvent<HTMLInputElement>) => {
const nextValue = event.target.value;
setSubmitError(null);
setValues((current) => ({
...current,
[key]: nextValue,
}));
},
[],
);

const handleToggleType = useCallback(() => {
setSubmitError(null);
setValues((current) => ({
...INITIAL_VALUES,
type: current.type === "signin" ? "signup" : "signin",
}));
}, []);

const handleSubmit = useCallback(
async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (isSubmitting || isInvalid) {
return;
}

setIsSubmitting(true);
setSubmitError(null);
try {
const error = await onSubmit(values);
if (error !== null) {
setSubmitError(error);
}
} finally {
setIsSubmitting(false);
}
},
[isInvalid, isSubmitting, onSubmit, values],
);
const type = currentType ?? initialValues.type;

return (
<form className="grid gap-y-6" onSubmit={handleSubmit}>
<h2 className="text-center text-2xl font-bold">
{type === "signin" ? "サインイン" : "新規登録"}
{values.type === "signin" ? "サインイン" : "新規登録"}
</h2>

<div className="flex justify-center">
<button
className="text-cax-brand underline"
onClick={() => change("type", type === "signin" ? "signup" : "signin")}
type="button"
>
{type === "signin" ? "初めての方はこちら" : "サインインはこちら"}
<button className="text-cax-brand underline" onClick={handleToggleType} type="button">
{values.type === "signin" ? "初めての方はこちら" : "サインインはこちら"}
</button>
</div>

<div className="grid gap-y-2">
<Field
name="username"
component={FormInputField}
props={{
label: "ユーザー名",
leftItem: <span className="text-cax-text-subtle leading-none">@</span>,
autoComplete: "username",
}}
/>

{type === "signup" && (
<Field
name="name"
component={FormInputField}
props={{
label: "名前",
autoComplete: "nickname",
}}
<div className="flex flex-col gap-y-1">
<label className="block text-sm" htmlFor="auth-username">
ユーザー名
</label>
<Input
id="auth-username"
aria-invalid={errors.username ? true : undefined}
autoComplete="username"
leftItem={<span className="text-cax-text-subtle leading-none">@</span>}
value={values.username}
onChange={handleChange("username")}
/>
{errors.username ? <span className="text-cax-danger text-xs">{errors.username}</span> : null}
</div>

{values.type === "signup" ? (
<div className="flex flex-col gap-y-1">
<label className="block text-sm" htmlFor="auth-name">
名前
</label>
<Input
id="auth-name"
aria-invalid={errors.name ? true : undefined}
autoComplete="nickname"
value={values.name}
onChange={handleChange("name")}
/>
{errors.name ? <span className="text-cax-danger text-xs">{errors.name}</span> : null}
</div>
) : null}

<div className="flex flex-col gap-y-1">
<label className="block text-sm" htmlFor="auth-password">
パスワード
</label>
<Input
id="auth-password"
aria-invalid={errors.password ? true : undefined}
autoComplete={values.type === "signup" ? "new-password" : "current-password"}
type="password"
value={values.password}
onChange={handleChange("password")}
/>
)}

<Field
name="password"
component={FormInputField}
props={{
label: "パスワード",
type: "password",
autoComplete: type === "signup" ? "new-password" : "current-password",
}}
/>
{errors.password ? <span className="text-cax-danger text-xs">{errors.password}</span> : null}
</div>
</div>

{type === "signup" ? (
{values.type === "signup" ? (
<p>
<Link className="text-cax-brand underline" onClick={onRequestCloseModal} to="/terms">
利用規約
Expand All @@ -85,19 +137,11 @@ const AuthModalPageComponent = ({
</p>
) : null}

<ModalSubmitButton disabled={submitting || invalid} loading={submitting}>
{type === "signin" ? "サインイン" : "登録する"}
<ModalSubmitButton disabled={isSubmitting || isInvalid} loading={isSubmitting}>
{values.type === "signin" ? "サインイン" : "登録する"}
</ModalSubmitButton>

<ModalErrorMessage>{error}</ModalErrorMessage>
<ModalErrorMessage>{submitError}</ModalErrorMessage>
</form>
);
};

export const AuthModalPage = reduxForm<AuthFormData, Props>({
form: "auth",
validate,
initialValues: {
type: "signin",
},
})(AuthModalPageComponent);
24 changes: 24 additions & 0 deletions application/client/src/components/crok/AssistantMarkdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import "katex/dist/katex.min.css";

import Markdown from "react-markdown";
import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";

import { CodeBlock } from "@web-speed-hackathon-2026/client/src/components/crok/CodeBlock";

interface Props {
content: string;
}

export const AssistantMarkdown = ({ content }: Props) => {
return (
<Markdown
components={{ pre: CodeBlock }}
rehypePlugins={[rehypeKatex]}
remarkPlugins={[remarkMath, remarkGfm]}
>
{content}
</Markdown>
);
};
Loading
Loading