diff --git a/app/components/LanguageSwitcher.tsx b/app/components/LanguageSwitcher.tsx new file mode 100644 index 0000000..99513e8 --- /dev/null +++ b/app/components/LanguageSwitcher.tsx @@ -0,0 +1,102 @@ +import { useTranslation } from "react-i18next"; +import { SUPPORTED_LANGUAGES } from "../lib/i18n"; + +export function LanguageSwitcher() { + const { t, i18n } = useTranslation(); + + const handleLanguageChange = (lng: string) => { + // Update URL to include/remove language prefix + const currentPath = window.location.pathname; + let newPath: string; + + if (lng === "ja") { + // Remove language prefix for Japanese (default) + if (currentPath.startsWith("/en")) { + newPath = currentPath.replace("/en", "") || "/"; + } else { + newPath = currentPath; + } + } else { + // Add language prefix for other languages + if (currentPath.startsWith("/en")) { + newPath = currentPath; + } else if (currentPath === "/") { + newPath = `/${lng}`; + } else { + newPath = `/${lng}${currentPath}`; + } + } + console.info( + `[LanguageSwitcher] changing language to ${lng}, newPath: ${newPath}`, + ); + + window.history.pushState({}, "", newPath); + + // FIXME: cookie 更新されてる? + // FIXME: ここで更新後、次回 http://localhost:5173/ にアクセスしたとき、自動で /en にリダイレクトさせたい + + // redirect to new path + window.location.href = newPath; + }; + + return ( +
+ + +
+ ); +} diff --git a/app/components/ThemeSwitcher.tsx b/app/components/ThemeSwitcher.tsx index 85a8792..8f97cdc 100644 --- a/app/components/ThemeSwitcher.tsx +++ b/app/components/ThemeSwitcher.tsx @@ -35,7 +35,11 @@ export function ThemeSwitcher() { className={`flex items-center gap-2 ${ currentTheme === theme.name ? "active" : "" }`} - onClick={() => handleThemeChange(theme.name)} + onClick={() => { + handleThemeChange(theme.name); + // ドロップダウンを閉じるためにフォーカスを外す + (document.activeElement as HTMLElement)?.blur(); + }} > {theme.label} {currentTheme === theme.name && ( diff --git a/app/components/icons/GitHubIconSvg.tsx b/app/components/icons/GitHubIconSvg.tsx new file mode 100644 index 0000000..b1d2624 --- /dev/null +++ b/app/components/icons/GitHubIconSvg.tsx @@ -0,0 +1,21 @@ +/** + * GitHub svg icon + * from simpleicons.org: https://simpleicons.org/?q=github + */ +export default function GitHubIconSvg({ + className = "w-5 h-5", +}: { + className?: string; +}) { + return ( + + GitHub + + + ); +} diff --git a/app/components/icons/LinkedInIconPng.tsx b/app/components/icons/LinkedInIconPng.tsx new file mode 100644 index 0000000..f14997c --- /dev/null +++ b/app/components/icons/LinkedInIconPng.tsx @@ -0,0 +1,11 @@ +/** + * LinkedIn png icon + */ +export default function LinkedInIconPng({ + className = "w-5 h-5", +}: { + className?: string; +}) { + const src = "/images/LinkedIn/LinkedInIconBackgroundBlack.png"; + return LinkedIn Icon; +} diff --git a/app/components/icons/NukopyIconPng.tsx b/app/components/icons/NukopyIconPng.tsx new file mode 100644 index 0000000..8171d8b --- /dev/null +++ b/app/components/icons/NukopyIconPng.tsx @@ -0,0 +1,11 @@ +/** + * Nukopy png icon + */ +export default function NukopyIconPng({ + className = "w-5 h-5", +}: { + className?: string; +}) { + const src = "/images/NukopyIcon.png"; + return Nucopy Icon; +} diff --git a/app/components/icons/XIconSvg.tsx b/app/components/icons/XIconSvg.tsx new file mode 100644 index 0000000..42b7496 --- /dev/null +++ b/app/components/icons/XIconSvg.tsx @@ -0,0 +1,21 @@ +/** + * X svg icon + * from simpleicons.org: https://simpleicons.org/?q=x + */ +export default function GitHubIconSvg({ + className = "w-5 h-5", +}: { + className?: string; +}) { + return ( + + X + + + ); +} diff --git a/app/components/icons/ZennIconSvg.tsx b/app/components/icons/ZennIconSvg.tsx new file mode 100644 index 0000000..0d23642 --- /dev/null +++ b/app/components/icons/ZennIconSvg.tsx @@ -0,0 +1,21 @@ +/** + * Zenn svg icon + * from simpleicons.org: https://simpleicons.org/?q=zenn + */ +export default function ZennIconSvg({ + className = "w-5 h-5", +}: { + className?: string; +}) { + return ( + + Zenn + + + ); +} diff --git a/app/components/icons/index.tsx b/app/components/icons/index.tsx new file mode 100644 index 0000000..16a8534 --- /dev/null +++ b/app/components/icons/index.tsx @@ -0,0 +1,31 @@ +import { BookOpenIcon, User } from "lucide-react"; +import type { JSX } from "react"; +import GitHubIconSvg from "./GitHubIconSvg"; +import LinkedInIconPng from "./LinkedInIconPng"; +import NukopyIconPng from "./NukopyIconPng"; +import XIconSvg from "./XIconSvg"; +import ZennIconSvg from "./ZennIconSvg"; + +export type IconType = "github" | "x" | "zenn" | "linkedin" | "blog" | "nukopy"; + +export function getIconByType( + iconType: IconType, + className?: string, +): JSX.Element { + switch (iconType) { + case "github": + return ; + case "x": + return ; + case "zenn": + return ; + case "linkedin": + return ; + case "blog": + return ; + case "nukopy": + return ; + default: + return ; + } +} diff --git a/app/components/layout/Header.tsx b/app/components/layout/Header.tsx index ee36efb..8f05025 100644 --- a/app/components/layout/Header.tsx +++ b/app/components/layout/Header.tsx @@ -1,11 +1,37 @@ +import { Link } from "react-router"; +import { LanguageSwitcher } from "../LanguageSwitcher"; import { ThemeSwitcher } from "../ThemeSwitcher"; +const MENU_ITEMS = [ + { + id: "contents", + href: "/contents", + label: "Contents", + }, + { + id: "tags", + href: "/tags", + label: "Tags", + }, + { + id: "about", + href: "/about", + label: "About", + }, + { + id: "misc", + href: "/misc", + label: "Misc", + }, +]; + export function Header() { return (
-
- -
    -
  • - Contents -
  • -
  • - Tags -
  • +
      + {MENU_ITEMS.map((item) => ( +
    • + { + // ドロップダウンを閉じるためにフォーカスを外す + (document.activeElement as HTMLElement)?.blur(); + }} + > + {item.label} + +
    • + ))}
- + + {/* logo */} + Chasing the Kernel - +
-
+
    -
  • - Contents -
  • -
  • - Tags -
  • + {MENU_ITEMS.map((item) => ( +
  • + {item.label} +
  • + ))}
+
); diff --git a/app/contents/hello-world.md b/app/contents/en/hello-world.md similarity index 76% rename from app/contents/hello-world.md rename to app/contents/en/hello-world.md index 38e5fa0..8fcc652 100644 --- a/app/contents/hello-world.md +++ b/app/contents/en/hello-world.md @@ -7,5 +7,5 @@ summary: "This is my first post!" # Hello world This is my first post! -... rest of the content -hoge +... rest of the content in English +hoge \ No newline at end of file diff --git a/app/contents/my-first-document.md b/app/contents/en/my-first-document.md similarity index 76% rename from app/contents/my-first-document.md rename to app/contents/en/my-first-document.md index 7b4991f..684974b 100644 --- a/app/contents/my-first-document.md +++ b/app/contents/en/my-first-document.md @@ -6,4 +6,4 @@ summary: "This is my first document!" # My first document -howhow... +This is my first document in English... \ No newline at end of file diff --git a/app/contents/test-mdx.mdx b/app/contents/en/test-mdx.mdx similarity index 92% rename from app/contents/test-mdx.mdx rename to app/contents/en/test-mdx.mdx index 91a2ba1..96ca558 100644 --- a/app/contents/test-mdx.mdx +++ b/app/contents/en/test-mdx.mdx @@ -8,4 +8,4 @@ import Counter from "./components/Counter.tsx"; # Test MDX - + \ No newline at end of file diff --git a/app/contents/ja/hello-world.md b/app/contents/ja/hello-world.md new file mode 100644 index 0000000..c1c2120 --- /dev/null +++ b/app/contents/ja/hello-world.md @@ -0,0 +1,11 @@ +--- +title: "こんにちは、世界" +tags: ["hello", "world"] +summary: "これははじめてのポストです。" +--- + +# こんにちは、世界 + +これははじめてのポストです。 + +ほげ diff --git a/app/contents/ja/my-first-document.md b/app/contents/ja/my-first-document.md new file mode 100644 index 0000000..01013fc --- /dev/null +++ b/app/contents/ja/my-first-document.md @@ -0,0 +1,9 @@ +--- +title: "はじめてのドキュメント" +tags: ["operating system", "unix"] +summary: "これははじめてのドキュメントです。" +--- + +# はじめてのドキュメント + +ほうほう diff --git a/app/contents/ja/test-mdx.mdx b/app/contents/ja/test-mdx.mdx new file mode 100644 index 0000000..3209b4d --- /dev/null +++ b/app/contents/ja/test-mdx.mdx @@ -0,0 +1,11 @@ +--- +title: "MDX のテスト" +tags: ["react", "mdx"] +summary: "MDX のテストです。" +--- + +import Counter from "./components/Counter.tsx"; + +# Test MDX + + diff --git a/app/contexts/cloudflareContext.ts b/app/contexts/cloudflareContext.ts new file mode 100644 index 0000000..1c39441 --- /dev/null +++ b/app/contexts/cloudflareContext.ts @@ -0,0 +1,9 @@ +import { unstable_createContext } from "react-router"; + +export const cloudflareContext = unstable_createContext<{ + env: Env; + ctx: ExecutionContext; +}>({ + env: {} as Env, + ctx: {} as ExecutionContext, +}); diff --git a/app/contexts/requestIdContext.ts b/app/contexts/requestIdContext.ts new file mode 100644 index 0000000..646b0d9 --- /dev/null +++ b/app/contexts/requestIdContext.ts @@ -0,0 +1,11 @@ +import { unstable_createContext } from "react-router"; +import type { RequestId } from "../types"; + +export const requestIdContext = unstable_createContext(null); + +// // 後でミドルウェア/ローダーで +// context.set(userContext, user); // User型である必要があります +// const user = context.get(userContext); // User型を返します + +// // ❌ 古い方法(型安全なし) +// // context.user = user; // 何でもあり得る diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 78cb542..46e0ea2 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -3,6 +3,27 @@ import { renderToReadableStream } from "react-dom/server"; import type { AppLoadContext, EntryContext } from "react-router"; import { ServerRouter } from "react-router"; +/** + * Server-side rendering entry point + * + * Steps (ref: https://zenn.dev/coji/articles/react-router-v7-internal-flow#%E8%B5%B7%E5%8B%95%E3%81%8B%E3%82%89%E5%88%9D%E5%9B%9E%E8%A1%A8%E7%A4%BA%E3%81%BE%E3%81%A7%E3%81%AE%E9%81%93%E3%81%AE%E3%82%8A-(ssr%E3%83%95%E3%83%AD%E3%83%BC)) + * (0. Run middlewares) + * 1. [server] [entrypoint of React Router] `fetch` function (workers/app.ts) + * 2. [server] route matching & run loader (app/rotues.ts & matched loaders on all matched routes) + * 3. [server] server-side rendering (SSR) (entry.server.tsx) + * 4. [server] send response, including rendered HTML stream, headers, etc., to client + * --- network --- + * 5. [client] receive response + * 6. [client] parse HTML and create DOM tree / load CSS and JavaScript modules + * 7. [client] run client entry point (app/entry.client.tsx) from `