diff --git a/app/layout.tsx b/app/layout.tsx
index 1697d61..e4f158c 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
+import Sidebar from "@/components/Sidebar";
import "./globals.css";
const geistSans = Geist({
@@ -31,11 +32,20 @@ export default function RootLayout({
lang="ja"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
-
-
- {children}
-
+
+ {/* 常駐型サイドバー */}
+
+
+ {/* メインエリア(ヘッダー・コンテンツ・フッター) */}
+
+
+
+ {children}
+
+
+
);
}
+
diff --git a/app/learn/nav-data.ts b/app/learn/nav-data.ts
new file mode 100644
index 0000000..7f0492c
--- /dev/null
+++ b/app/learn/nav-data.ts
@@ -0,0 +1,55 @@
+export type LanguageSection = {
+ sectionSlug: string;
+ title: string;
+};
+
+export type LanguageData = {
+ languageSlug: string;
+ label: string;
+ sections: LanguageSection[];
+};
+
+export const LANGUAGES_DATA: LanguageData[] = [
+ {
+ languageSlug: "chinese",
+ label: "中国語",
+ sections: [{ sectionSlug: "01", title: "名詞の性の語尾による判別" }],
+ },
+ {
+ languageSlug: "french",
+ label: "フランス語",
+ sections: [
+ { sectionSlug: "01", title: "冠詞の一覧と縮約形" },
+ { sectionSlug: "02", title: "基本動詞の直説法現在活用" },
+ { sectionSlug: "03", title: "基数と序数と概数" },
+ { sectionSlug: "04", title: "時刻表現" },
+ { sectionSlug: "05", title: "日程表現" },
+ { sectionSlug: "06", title: "人称代名詞" },
+ { sectionSlug: "07", title: "所有形容詞" },
+ { sectionSlug: "08", title: "所有代名詞" },
+ { sectionSlug: "09", title: "指示形容詞" },
+ { sectionSlug: "10", title: "指示代名詞" },
+ { sectionSlug: "11", title: "主な否定表現" },
+ { sectionSlug: "12", title: "疑問詞一覧" },
+ { sectionSlug: "13", title: "命令形の不規則活用" },
+ { sectionSlug: "14", title: "直説法半過去形の活用" },
+ { sectionSlug: "15", title: "直説法単純未来形の活用" },
+ { sectionSlug: "16", title: "比較級で特殊な変化をする形容詞・副詞" },
+ { sectionSlug: "17", title: "現在分詞・過去分詞の作り方" },
+ { sectionSlug: "18", title: "条件法現在形の活用" },
+ { sectionSlug: "19", title: "接続法現在形の活用" },
+ { sectionSlug: "20", title: "不定表現一覧" },
+ { sectionSlug: "21", title: "中性代名詞一覧" },
+ ],
+ },
+ {
+ languageSlug: "german",
+ label: "ドイツ語",
+ sections: [{ sectionSlug: "01", title: "名詞の性の語尾による判別" }],
+ },
+ {
+ languageSlug: "spanish",
+ label: "スペイン語",
+ sections: [{ sectionSlug: "01", title: "名詞の性の語尾による判別" }],
+ },
+];
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx
new file mode 100644
index 0000000..11d8f7e
--- /dev/null
+++ b/components/Sidebar.tsx
@@ -0,0 +1,195 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useState, useEffect } from "react";
+import { LANGUAGES_DATA } from "@/app/learn/nav-data";
+
+export default function Sidebar() {
+ const pathname = usePathname();
+
+ // 1階層目 (学習教材) の開閉状態
+ const [isLearnOpen, setIsLearnOpen] = useState(true);
+
+ // 2階層目 (言語) ごとの開閉状態管理
+ const [openLanguages, setOpenLanguages] = useState>(() => {
+ const initial: Record = {};
+ LANGUAGES_DATA.forEach((lang) => {
+ initial[lang.languageSlug] = pathname.startsWith(`/learn/${lang.languageSlug}`);
+ });
+ return initial;
+ });
+
+ // 初期ロード時のみアクティブな言語を展開(手動開閉操作を尊重する)
+ useEffect(() => {
+ if (pathname.startsWith("/learn")) {
+ LANGUAGES_DATA.forEach((lang) => {
+ if (pathname.startsWith(`/learn/${lang.languageSlug}`)) {
+ setOpenLanguages((prev) => {
+ if (prev[lang.languageSlug] === undefined) {
+ return { ...prev, [lang.languageSlug]: true };
+ }
+ return prev;
+ });
+ }
+ });
+ }
+ }, []);
+
+
+ const toggleLanguage = (slug: string) => {
+ setOpenLanguages((prev) => ({ ...prev, [slug]: !prev[slug] }));
+ };
+
+ return (
+
+ );
+}
+
+
diff --git a/dev.db b/dev.db
new file mode 100644
index 0000000..008aedc
Binary files /dev/null and b/dev.db differ
diff --git a/lib/prisma.ts b/lib/prisma.ts
index 88f7beb..32f0793 100644
--- a/lib/prisma.ts
+++ b/lib/prisma.ts
@@ -1,6 +1,8 @@
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient } from "@/app/generated/prisma/client";
+
+
// 開発モードではファイル変更のたびにモジュールが再評価され、
// その都度 PrismaClient を new すると接続数が増え続けてしまう。
// globalThis にインスタンスを退避し、HMR をまたいで使い回す。
@@ -8,7 +10,8 @@ const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
-const adapter = new PrismaBetterSqlite3({ url: process.env.DATABASE_URL! });
+const dbUrl = process.env.DATABASE_URL || "file:./dev.db";
+const adapter = new PrismaBetterSqlite3({ url: dbUrl });
export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });
diff --git a/package-lock.json b/package-lock.json
index 4105186..58f3d58 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2312,7 +2312,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
diff --git a/prisma.config.ts b/prisma.config.ts
index 831a20f..8a966a5 100644
--- a/prisma.config.ts
+++ b/prisma.config.ts
@@ -9,6 +9,6 @@ export default defineConfig({
path: "prisma/migrations",
},
datasource: {
- url: process.env["DATABASE_URL"],
+ url: process.env["DATABASE_URL"] || "file:./dev.db",
},
});