Skip to content
Draft
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
13 changes: 11 additions & 2 deletions app/components/note/Note.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Badge, Button, Card, Group, Stack, Text } from "@mantine/core";
import { useMemo } from "react";
import type { Tweet } from "react-tweet/api";
import { EmbeddedTweet } from "react-tweet/patched/components/embedded-tweet";

import { LANGUAGE_ID_TO_LABEL } from "../../feature/search/language";
import {
Expand All @@ -14,9 +16,10 @@ import { NoteTopic } from "./NoteTopics";

type NoteProps = {
note: SearchedNote;
fetchedPost: Tweet | undefined;
};

export const Note = ({ note }: NoteProps) => {
export const Note = ({ note, fetchedPost }: NoteProps) => {
const dateString = useMemo(() => {
return new Date(note.createdAt).toLocaleString("ja-JP", {
year: "numeric",
Expand Down Expand Up @@ -60,7 +63,13 @@ export const Note = ({ note }: NoteProps) => {
<NoteTopic topics={note.topics} />
</div>
</Stack>
<Post post={note.post} />
{note.post != null ? (
<Post post={note.post} />
) : fetchedPost != null ? (
<div data-theme="light">
<EmbeddedTweet tweet={fetchedPost} />
</div>
) : null}
{isNonEmptyString(note.postId) && (
<Group justify="flex-end">
<Button
Expand Down
10 changes: 8 additions & 2 deletions app/components/note/Notes.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { memo } from "react";
import type { Tweet } from "react-tweet/api";

import type { SearchedNote } from "../../generated/api/schemas";
import { Note } from "./Note";

type NotesProps = {
notes: SearchedNote[];
fetchedPosts: Record<string, Tweet>;
};

export const Notes = memo(({ notes }: NotesProps) => {
export const Notes = memo(({ notes, fetchedPosts }: NotesProps) => {
return (
<>
{notes.map((note) => (
<Note key={note.noteId} note={note} />
<Note
fetchedPost={fetchedPosts[note.postId]}
key={note.noteId}
note={note}
/>
))}
</>
);
Expand Down
4 changes: 3 additions & 1 deletion app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import { mantineTheme } from "./config/mantine";
dayjs.extend(customParseFormat);

export function Layout({ children }: { children: React.ReactNode }) {
// mantine の <ColorSchemeScript /> がブラウザ上でのみ
// html に data-mantine-color-scheme を追加するので suppressHydrationWarning を付けている
return (
<html lang="ja">
<html lang="ja" suppressHydrationWarning>
<head>
<meta charSet="utf-8" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
Expand Down
42 changes: 31 additions & 11 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { parseWithZod } from "@conform-to/zod";
import { Anchor, Card, Container, Divider, Group, Stack } from "@mantine/core";
import { data, Link, redirect, useNavigation } from "react-router";
import { getTweet, type Tweet } from "react-tweet/api";
import { getQuery, withQuery } from "ufo";

import Fa6SolidMagnifyingGlass from "~icons/fa6-solid/magnifying-glass";
Expand All @@ -13,7 +14,7 @@ import {
getTopicsApiV1DataTopicsGet,
searchApiV1DataSearchGet,
} from "../generated/api/client";
import type { SearchedNote, Topic } from "../generated/api/schemas";
import { isNonEmptyString } from "../utils/string";
import type { Route } from "./+types/_index";

export const meta: Route.MetaFunction = () => {
Expand Down Expand Up @@ -58,6 +59,7 @@ export const loader = async (args: Route.LoaderArgs) => {
},
},
topics: [],
fetchedPosts: {},
},
error: searchQuery.error.errors,
},
Expand All @@ -74,11 +76,36 @@ export const loader = async (args: Route.LoaderArgs) => {
searchApiV1DataSearchGet(searchQuery.data),
]);

for (const note of response.data.data) {
note.post = null;
}

const postIdsToFetch: string[] = [];
for (const note of response.data.data) {
// post が未取得で postId が存在する場合は BFF でポスト情報を一括 fetch したい
if (note.post != null || !isNonEmptyString(note.postId)) {
continue;
}
postIdsToFetch.push(note.postId);
}

const posts = await Promise.all(
postIdsToFetch.map(async (id) => getTweet(id)),
);
const postsMap: Record<string, Tweet> = {};
for (const post of posts) {
if (!post) {
continue;
}
postsMap[post.id_str] = post;
}

return {
data: {
searchQuery: searchQuery.data,
searchResults: response.data,
topics: topics.data.data,
fetchedPosts: postsMap,
},
error: null,
};
Expand All @@ -93,6 +120,7 @@ export default function Index({
const {
topics,
searchQuery,
fetchedPosts,
searchResults: { data: notes, meta: paginationMeta },
} = loaderData.data;

Expand All @@ -111,10 +139,7 @@ export default function Index({
<SearchForm
defaultValue={searchQuery ?? undefined}
lastResult={actionData}
topics={
// react-router の型がうまく機能せず topics が unknown になったため
topics as Topic[]
}
topics={topics}
/>
</div>
<Divider className="md:hidden" />
Expand All @@ -133,12 +158,7 @@ export default function Index({
/>
)}
<Group gap="lg">
<Notes
notes={
// react-router の型がうまく機能せず notes[number].topics が unknown になったため
notes as SearchedNote[]
}
/>
<Notes fetchedPosts={fetchedPosts} notes={notes} />
</Group>
{searchQuery && (
<SearchPagination
Expand Down
6 changes: 2 additions & 4 deletions eslint.config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="./eslint-typegen.d.ts" />

import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js";
import type { Linter } from "eslint";
import gitignore from "eslint-config-flat-gitignore";
import jsxA11y from "eslint-plugin-jsx-a11y";
import react from "eslint-plugin-react";
import * as reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import typegen from "eslint-typegen";
Expand All @@ -18,8 +18,6 @@ const tsFiles = "**/*.{ts,tsx,mts,cts}";
const jsFiles = "**/*.{js,jsx,mjs,cjs}";
const jsxFiles = "**/*.{jsx,tsx}";

const compat = new FlatCompat();

const config = [
gitignore(),
{
Expand Down Expand Up @@ -106,7 +104,7 @@ const config = [
react.configs.flat.recommended,
// @ts-expect-error 型が合わない
react.configs.flat["jsx-runtime"],
...compat.extends("plugin:react-hooks/recommended"),
reactHooks.configs["recommended-latest"],
reactRefresh.configs.vite,
jsxA11y.flatConfigs.recommended,
],
Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"lint": "eslint --max-warnings 0",
"format": "prettier --write .",
"format:ci": "prettier --check .",
"start": "react-router-serve ./build/server/index.js",
"start": "react-router-serve ./build/server/nodejs_*/index.js",
"typecheck": "pnpm run typegen && tsc --noEmit",
"typegen": "react-router typegen",
"orval": "orval",
Expand Down Expand Up @@ -39,6 +39,7 @@
"react-dom": "19.0.0",
"react-router": "7.3.0",
"react-router-dom": "7.3.0",
"react-tweet": "3.2.2",
"recharts": "2.15.1",
"ufo": "1.5.4",
"zod": "3.24.2"
Expand Down Expand Up @@ -91,14 +92,17 @@
"engines": {
"node": "^22.0.0"
},
"packageManager": "pnpm@10.5.2",
"packageManager": "pnpm@10.6.1",
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"msw"
],
"overrides": {
"react-is": "19.0.0"
},
"patchedDependencies": {
"react-tweet": "patches/react-tweet.patch"
}
}
}
23 changes: 23 additions & 0 deletions patches/react-tweet.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
diff --git a/package.json b/package.json
index 36145877a0f6bc95d2156afbc2c21ebbaeb1ba13..d4b6f8bcfbcd6e20e3f24ba68549d01762fef1fe 100644
--- a/package.json
+++ b/package.json
@@ -17,8 +17,17 @@
"default": "./dist/index.client.js"
},
"./api": "./dist/api/index.js",
- "./theme.css": "./dist/twitter-theme/theme.css"
+ "./theme.css": "./dist/twitter-theme/theme.css",
+ "./patched/components/embedded-tweet": {
+ "default": "./dist/twitter-theme/embedded-tweet.js",
+ "types": "./dist/twitter-theme/embedded-tweet.d.ts"
+ }
},
+ "//": [
+ "./patched/components/embedded-tweet is escape hatch to import EmbeddedTweet component.",
+ "import { EmbeddedTweet } from 'react-tweet' causes error in Vite + React Router",
+ "because it has double barrel file and import alanysis is not working properly."
+ ],
"files": [
"dist/**/*.{js,d.ts,css}"
],
49 changes: 49 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,11 @@ export default defineConfig({
}),
tsconfigPaths(),
],

ssr: {
noExternal: [
// react-tweet 内部の css の import が壊れるので ssr.noExternal に含める
"react-tweet"
],
},
});