Skip to content
Merged
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
6 changes: 3 additions & 3 deletions app/(docs)/@chat/chat/[chatId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "@/lib/chatHistory";
import { getMarkdownSections, getPagesList } from "@/lib/docs";
import { ChatAreaContainer, ChatAreaContent } from "./chatArea";
import { unstable_cacheLife, unstable_cacheTag } from "next/cache";
import { cacheLife, cacheTag } from "next/cache";
import { isCloudflare } from "@/lib/detectCloudflare";

export default async function ChatPage({
Expand Down Expand Up @@ -56,8 +56,8 @@ export default async function ChatPage({

async function getChatOneFromCache(chatId: string, userId?: string) {
"use cache";
unstable_cacheLife("days");
unstable_cacheTag(cacheKeyForChat(chatId));
cacheLife("days");
cacheTag(cacheKeyForChat(chatId));

if (!userId) {
return null;
Expand Down
6 changes: 3 additions & 3 deletions app/(docs)/@docs/[lang]/[pageId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
PagePath,
PageSlug,
} from "@/lib/docs";
import { unstable_cacheLife, unstable_cacheTag } from "next/cache";
import { cacheLife, cacheTag } from "next/cache";
import { isCloudflare } from "@/lib/detectCloudflare";
import { DocsAutoRedirect } from "./autoRedirect";

Expand Down Expand Up @@ -83,12 +83,12 @@ async function getChatFromCache(path: PagePath, userId?: string) {
// 一方、use cacheの関数内でheaders()にはアクセスできない。
// したがって、外でheaders()を使ってuserIdを取得した後、関数の中で再度drizzleを初期化しないといけない。
"use cache";
unstable_cacheLife("days");
cacheLife("days");

if (!userId) {
return [];
}
unstable_cacheTag(cacheKeyForPage(path, userId));
cacheTag(cacheKeyForPage(path, userId));

if (isCloudflare()) {
const cache = await caches.open("chatHistory");
Expand Down
87 changes: 38 additions & 49 deletions app/(docs)/@docs/[lang]/[pageId]/pageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { Fragment, useCallback, useEffect, useRef, useState } from "react";
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
import { ChatForm } from "./chatForm";
import { StyledMarkdown } from "@/markdown/markdown";
import { useSidebarMdContext } from "@/sidebar";
Expand Down Expand Up @@ -32,11 +32,41 @@ export function PageContent(props: PageContentProps) {
const { setSidebarMdContent } = useSidebarMdContext();
const { splitMdContent, pageEntry, path, chatHistories } = props;

const initDynamicMdContent = useCallback(() => {
const [sectionInView, setSectionInView] = useState<boolean[]>([]);
const sectionRefs = useRef<Array<HTMLDivElement | null>>([]);
useEffect(() => {
const handleScroll = () => {
setSectionInView((sectionInView) => {
sectionInView = sectionInView.slice(); // Reactの変更検知のために新しい配列を作成
for (
let i = 0;
i < sectionRefs.current.length || i < sectionInView.length;
i++
) {
if (sectionRefs.current.at(i)) {
const rect = sectionRefs.current.at(i)!.getBoundingClientRect();
sectionInView[i] =
rect.top < window.innerHeight * 0.9 &&
rect.bottom >= window.innerHeight * 0.1;
} else {
sectionInView[i] = false;
}
}
return sectionInView;
});
};
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);

const dynamicMdContent = useMemo(() => {
const newContent: DynamicMarkdownSection[] = splitMdContent.map(
(section) => ({
(section, i) => ({
...section,
inView: false,
inView: sectionInView[i],
replacedContent: section.rawContent,
replacedRange: [],
})
Expand Down Expand Up @@ -94,54 +124,13 @@ export function PageContent(props: PageContentProps) {
}

return newContent;
}, [splitMdContent, chatHistories]);

// SSR用のローカルstate
const [dynamicMdContent, setDynamicMdContent] = useState<
DynamicMarkdownSection[]
>(() => initDynamicMdContent());
}, [splitMdContent, chatHistories, sectionInView]);

useEffect(() => {
// props.splitMdContentが変わったとき, チャットのdiffが変わった時に
// ローカルstateとcontextの両方を更新
const newContent = initDynamicMdContent();
setDynamicMdContent(newContent);
setSidebarMdContent(path, newContent);
}, [initDynamicMdContent, path, setSidebarMdContent]);

const sectionRefs = useRef<Array<HTMLDivElement | null>>([]);
// sectionRefsの長さをsplitMdContentに合わせる
while (sectionRefs.current.length < props.splitMdContent.length) {
sectionRefs.current.push(null);
}

useEffect(() => {
const handleScroll = () => {
const updateContent = (
prevDynamicMdContent: DynamicMarkdownSection[]
) => {
const dynMdContent = prevDynamicMdContent.slice(); // Reactの変更検知のために新しい配列を作成
for (let i = 0; i < sectionRefs.current.length; i++) {
if (sectionRefs.current.at(i) && dynMdContent.at(i)) {
const rect = sectionRefs.current.at(i)!.getBoundingClientRect();
dynMdContent.at(i)!.inView =
rect.top < window.innerHeight * 0.9 &&
rect.bottom >= window.innerHeight * 0.1;
}
}
return dynMdContent;
};

// ローカルstateとcontextの両方を更新
setDynamicMdContent(updateContent);
setSidebarMdContent(path, updateContent);
};
window.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [setSidebarMdContent, path]);
// sidebarのcontextを更新
setSidebarMdContent(path, dynamicMdContent);
}, [dynamicMdContent, path, setSidebarMdContent]);

const [isFormVisible, setIsFormVisible] = useState(false);

Expand Down
9 changes: 4 additions & 5 deletions app/lib/chatHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getDrizzle } from "./drizzle";
import { chat, diff, message, section } from "@/schema/chat";
import { and, asc, eq, exists } from "drizzle-orm";
import { Auth } from "better-auth";
import { revalidateTag } from "next/cache";
import { updateTag } from "next/cache";
import { isCloudflare } from "./detectCloudflare";
import { getPagesList, LangId, PagePath, PageSlug, SectionId } from "./docs";

Expand All @@ -31,7 +31,6 @@ export function cacheKeyForChat(chatId: string) {
// nextjsのキャッシュのrevalidateはRouteHandlerではなくServerActionから呼ばないと正しく動作しないらしい。
// https://github.com/vercel/next.js/issues/69064
// そのためlib/以下の関数では直接revalidateChatを呼ばず、ServerActionの関数から呼ぶようにする。
// Nextjs 16 に更新したらこれをupdateTag()で置き換える。
export async function revalidateChat(
chatId: string,
userId: string,
Expand All @@ -41,8 +40,8 @@ export async function revalidateChat(
const [lang, page] = pagePath.split("/") as [LangId, PageSlug];
pagePath = { lang, page };
}
revalidateTag(cacheKeyForChat(chatId));
revalidateTag(cacheKeyForPage(pagePath, userId));
updateTag(cacheKeyForChat(chatId));
updateTag(cacheKeyForPage(pagePath, userId));
if (isCloudflare()) {
const cache = await caches.open("chatHistory");
await cache.delete(cacheKeyForChat(chatId));
Expand Down Expand Up @@ -283,7 +282,7 @@ export async function migrateChatUser(oldUserId: string, newUserId: string) {
const pagesList = await getPagesList();
for (const lang of pagesList) {
for (const page of lang.pages) {
revalidateTag(
updateTag(
cacheKeyForPage({ lang: lang.id, page: page.slug }, newUserId)
);
}
Expand Down
12 changes: 12 additions & 0 deletions app/lib/clientOnly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

import { useSyncExternalStore } from "react";

// --- SSR無効化のためのカスタムフック準備 ---
const subscribe = () => () => {};
const getSnapshot = () => true; // クライアントでは true
const getServerSnapshot = () => false; // サーバーでは false

export function useIsClient() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
10 changes: 4 additions & 6 deletions app/markdown/styledSyntaxHighlighter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import {
tomorrow,
tomorrowNight,
} from "react-syntax-highlighter/dist/esm/styles/hljs";
import { lazy, Suspense, useEffect, useState } from "react";
import { lazy, Suspense } from "react";
import { LangConstants } from "@my-code/runtime/languages";
import clsx from "clsx";
import { useIsClient } from "@/lib/clientOnly";

// SyntaxHighlighterはファイルサイズがでかいので & HydrationErrorを起こすので、SSRを無効化する
const SyntaxHighlighter = lazy(() => {
Expand All @@ -24,11 +25,8 @@ export function StyledSyntaxHighlighter(props: {
}) {
const theme = useChangeTheme();
const codetheme = theme === "tomorrow" ? tomorrow : tomorrowNight;
const [initHighlighter, setInitHighlighter] = useState(false);
useEffect(() => {
setInitHighlighter(true);
}, []);
return initHighlighter ? (
const isClient = useIsClient();
return isClient ? (
<Suspense fallback={<FallbackPre>{props.children}</FallbackPre>}>
<SyntaxHighlighter
language={props.language.rsh}
Expand Down
28 changes: 11 additions & 17 deletions app/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
ReactNode,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import clsx from "clsx";
Expand Down Expand Up @@ -100,29 +99,24 @@

// 目次の開閉状態
const [detailsOpen, setDetailsOpen] = useState<boolean[]>([]);
const currentLangIndex = pagesList.findIndex(
(group) => currentLang === group.id
);
useEffect(() => {
// 表示しているグループが変わったときに現在のグループのdetailsを開く
if (currentLangIndex !== -1) {
setDetailsOpen((detailsOpen) => {
const newDetailsOpen = [...detailsOpen];
while (newDetailsOpen.length <= currentLangIndex) {
newDetailsOpen.push(false);
}
newDetailsOpen[currentLangIndex] = true;
return newDetailsOpen;
});
}
}, [currentLangIndex]);
const [prevLangIndex, setPrevLangIndex] = useState<number>(-1);
const langIndex = pagesList.findIndex((group) => currentLang === group.id);
// 表示しているグループが変わったときに現在のグループのdetailsを開く
if (prevLangIndex !== langIndex) {
setPrevLangIndex(langIndex);
setDetailsOpen((detailsOpen) => {
const newDetailsOpen = [...detailsOpen];
newDetailsOpen[langIndex] = true;
return newDetailsOpen;
});
}

return (
<div className="bg-base-200 h-full w-sidebar flex flex-col relative">
<h2 className="hidden has-sidebar:flex flex-row items-center p-4 gap-2">
{/* サイドバーが常時表示されている場合のみ */}
<Link href="/" className="flex-1 flex items-center">
<img

Check warning on line 119 in app/sidebar.tsx

View workflow job for this annotation

GitHub Actions / lint (22.x)

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src="/icon.svg"
alt="my.code(); Logo"
className="inline-block w-8 h-8 mr-1"
Expand Down
18 changes: 7 additions & 11 deletions app/terminal/embedContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
ReactNode,
useCallback,
useContext,
useEffect,
useState,
} from "react";

Expand Down Expand Up @@ -58,7 +57,7 @@ export function useEmbedContext() {
export function EmbedContextProvider({ children }: { children: ReactNode }) {
const pathname = usePathname();

const [currentPathname, setCurrentPathname] = useState<PagePathname>("");
const [prevPathname, setPrevPathname] = useState<PagePathname>("");
const [files, setFiles] = useState<
Record<PagePathname, Record<Filename, string>>
>({});
Expand All @@ -72,15 +71,12 @@ export function EmbedContextProvider({ children }: { children: ReactNode }) {
const [execResults, setExecResults] = useState<
Record<Filename, ReplOutput[]>
>({});
// pathnameが変わったらデータを初期化
useEffect(() => {
if (pathname && pathname !== currentPathname) {
setCurrentPathname(pathname);
setReplOutputs({});
setCommandIdCounters({});
setExecResults({});
}
}, [pathname, currentPathname]);
if (pathname && pathname !== prevPathname) {
setPrevPathname(pathname);
setReplOutputs({});
setCommandIdCounters({});
setExecResults({});
}

const writeFile = useCallback(
(updatedFiles: Record<Filename, string>) => {
Expand Down
2 changes: 1 addition & 1 deletion app/terminal/exec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ export function ExecFile(props: ExecProps) {
>("idle");
useEffect(() => {
if (executionState === "triggered" && ready) {
setExecutionState("executing");
(async () => {
setExecutionState("executing");
clearTerminal(terminalInstanceRef.current!);
terminalInstanceRef.current!.write(systemMessageColor("実行中です..."));
// TODO: 1つのファイル名しか受け付けないところに無理やりコンマ区切りで全部のファイル名を突っ込んでいる
Expand Down
8 changes: 6 additions & 2 deletions app/terminal/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Heading } from "@/markdown/heading";
import "mocha/mocha.css";
import { Fragment, useEffect, useState } from "react";
import { Fragment, useEffect, useRef, useState } from "react";
import { langConstants, RuntimeLang } from "@my-code/runtime/languages";
import { ReplTerminal } from "./repl";
import { EditorComponent } from "./editor";
Expand Down Expand Up @@ -202,7 +202,11 @@ function AnsiColorSample() {
}

function MochaTest() {
const runtimeRef = useRuntimeAll();
const runtimeAll = useRuntimeAll();
const runtimeRef = useRef(runtimeAll);
for (const lang of Object.keys(runtimeAll) as RuntimeLang[]) {
runtimeRef.current[lang] = runtimeAll[lang];
}

const [searchParams, setSearchParams] = useState<string>("");
useEffect(() => {
Expand Down
31 changes: 18 additions & 13 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
baseDirectory: __dirname,
});
import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypescript from "eslint-config-next/typescript";
import reactHooks from "eslint-plugin-react-hooks";

const eslintConfig = [
...compat.config({
extends: ["next/core-web-vitals", "next/typescript"],
{
ignores: [
".next/**",
".open-next/**",
".wrangler/**",
"node_modules/**",
"public/**",
"cloudflare-env.d.ts",
],
},
...nextCoreWebVitals,
...nextTypescript,
{
plugins: { "react-hooks": reactHooks },
rules: {
// Next.jsのデフォルト設定を上書き
"@typescript-eslint/no-unused-vars": [
Expand All @@ -23,7 +28,7 @@ const eslintConfig = [
},
],
},
}),
},
];

export default eslintConfig;
Loading
Loading