From 351a92de006d9a232e01d2b98fd71ed61eaa08f9 Mon Sep 17 00:00:00 2001
From: "R.H" <312978554+6756432598-creator@users.noreply.github.com>
Date: Fri, 18 Sep 2026 10:09:24 +0900
Subject: [PATCH 1/4] =?UTF-8?q?=E3=83=86=E3=83=B3=E3=83=97=E3=83=AC?=
=?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=A8=E5=8D=98=E8=AA=9E=E8=BF=BD=E5=8A=A0?=
=?UTF-8?q?=E6=A9=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/my-notebooks/page.tsx | 16 ++++-
app/my-notebooks/template/route.ts | 47 ++++++++++++++
components/my-notebooks/ImportForm.tsx | 84 ++++++++++++++++++++++++++
lib/excel.ts | 8 +++
4 files changed, 153 insertions(+), 2 deletions(-)
create mode 100644 app/my-notebooks/template/route.ts
diff --git a/app/my-notebooks/page.tsx b/app/my-notebooks/page.tsx
index bf108a2..87e589b 100644
--- a/app/my-notebooks/page.tsx
+++ b/app/my-notebooks/page.tsx
@@ -1,7 +1,7 @@
import Link from "next/link";
import { prisma } from "@/lib/prisma";
-import ImportForm from "@/components/my-notebooks/ImportForm";
+import ImportForm, {ImportTemplate,} from "@/components/my-notebooks/ImportForm";
import DeleteNotebookButton from "@/components/my-notebooks/DeleteNotebookButton";
import StarColorSettings from "@/components/StarColorSettings";
@@ -30,13 +30,25 @@ export default async function MyNotebooksPage() {
+ {/*以下の2つのセクションをパソコンでは横並びで、スマホではたて並びで表示する*/}
+
+
- Excelから新規作成
+ 自分のExcelから新規作成
+
+
+
+
作成済みの単語帳
diff --git a/app/my-notebooks/template/route.ts b/app/my-notebooks/template/route.ts
new file mode 100644
index 0000000..7d66d5e
--- /dev/null
+++ b/app/my-notebooks/template/route.ts
@@ -0,0 +1,47 @@
+import { NextRequest, NextResponse } from "next/server";
+import ExcelJS from "exceljs";
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = new URL(request.url);
+ const filename = searchParams.get("filename") || "単語帳テンプレート";
+ const language = searchParams.get("language") || "英語";
+
+const languageMap: Record = {
+ 英語: { col1: "単語", col2: "意味", col3: "", col4: "", col5: "", col6: "", col1_2: "example", col2_2: "例", col3_2: "", col4_2: "", col5_2: "", col6_2: "" },
+ ドイツ語: { col1: "単語", col2: "意味", col3: "性", col4: "複数形", col5: "格・前置詞", col6: "", col1_2: "Beispiel", col2_2: "例", col3_2: "das Beispiel", col4_2: "die Beispiele", col5_2: "", col6_2: "" },
+ 中国語: { col1: "単語", col2: "意味", col3: "拼音", col4: "", col5: "", col6: "", col1_2: "例子", col2_2: "例", col3_2: "lìzi", col4_2: "", col5_2: "", col6_2: "" },
+ フランス語: { col1: "単語", col2: "意味", col3: "性", col4: "格・前置詞", col5: "", col6: "", col1_2: "exemple", col2_2: "例", col3_2: "un exemple", col4_2: "", col5_2: "", col6_2: "" },
+ スペイン語: { col1: "単語", col2: "意味", col3: "性", col4: "格・前置詞", col5: "", col6: "", col1_2: "ejemplo", col2_2: "例", col3_2: "el ejemplo", col4_2: "", col5_2: "", col6_2: "" },
+ };
+
+ const template = languageMap[language] ?? languageMap["英語"];
+
+
+ const workbook = new ExcelJS.Workbook();
+ const worksheet = workbook.addWorksheet("Sheet1");
+
+ worksheet.columns = [
+ { header: template.col1, key: "col1", width: 20 },
+ { header: template.col2, key: "col2", width: 20 },
+ { header: template.col3, key: "col3", width: 20 },
+ { header: template.col4, key: "col4", width: 20 },
+ { header: template.col5, key: "col5", width: 20 },
+ { header: template.col6, key: "col6", width: 20 },
+ ];
+
+ worksheet.addRow([template.col1_2, template.col2_2, template.col3_2, template.col4_2, template.col5_2, template.col6_2]);
+ worksheet.addRow(["", "", "", "", "", ""]);
+
+ const fileName = filename.endsWith(".xlsx") ? filename : `${filename}.xlsx`;
+
+ const buffer = await workbook.xlsx.writeBuffer();
+
+ return new NextResponse(buffer, {
+ status: 200,
+ headers: {
+ "Content-Type":
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ "Content-Disposition": `attachment; filename="${fileName}"`,
+ },
+ });
+}
\ No newline at end of file
diff --git a/components/my-notebooks/ImportForm.tsx b/components/my-notebooks/ImportForm.tsx
index e913a3f..fbaaed9 100644
--- a/components/my-notebooks/ImportForm.tsx
+++ b/components/my-notebooks/ImportForm.tsx
@@ -2,11 +2,20 @@
import { useActionState } from "react";
import { useFormStatus } from "react-dom";
+import { useState } from "react";
import { importNotebookFromExcel, type FormState } from "@/app/my-notebooks/actions";
const initialState: FormState = {};
+const languageOptions = [
+ "フランス語",
+ "ドイツ語",
+ "スペイン語",
+ "中国語",
+ "英語",
+];
+
// フォーム送信中はボタンを disabled にし、ラベルを差し替える
function SubmitButton() {
const { pending } = useFormStatus();
@@ -22,6 +31,81 @@ function SubmitButton() {
);
}
+export function ImportTemplate() {
+ // useActionStateは、Server Actionの戻り値({ error }など)を
+ // 前回の実行結果として保持してくれるReactのフック
+ const [state, formAction] = useActionState(importNotebookFromExcel, initialState);
+
+ const [templateName, setTemplateName] = useState("");
+ const [selectedLanguage, setSelectedLanguage] = useState("英語");
+
+ const downloadTemplate = async () => {
+ const lang = selectedLanguage ?? "英語";
+ const url = `/my-notebooks/template?filename=${encodeURIComponent(templateName || "単語帳テンプレート")}&language=${encodeURIComponent(lang)}`;
+ window.location.href = url;
+ };
+
+ return (
+
+
+
+ setTemplateName(e.target.value)}
+ placeholder="例: French_1"
+ className="rounded-lg border border-black/[.08] bg-transparent px-3 py-2 text-sm outline-none focus:border-black/[.3] dark:border-white/[.145] dark:focus:border-white/[.4]"
+ />
+
+
+
+
+ 言語を選択
+
+
+ {languageOptions.map((language) => {
+ const isSelected = selectedLanguage === language;
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {state?.error && (
+
+ {state.error}
+
+ )}
+
+ );
+}
+
+
export default function ImportForm() {
// useActionStateは、Server Actionの戻り値({ error }など)を
// 前回の実行結果として保持してくれるReactのフック
diff --git a/lib/excel.ts b/lib/excel.ts
index ca8fdaa..414ddfc 100644
--- a/lib/excel.ts
+++ b/lib/excel.ts
@@ -13,6 +13,14 @@ export type ParsedNotebook = {
export class ExcelParseError extends Error {}
+export async function createTemplateWorkbookBuffer(): Promise {
+ const workbook = new ExcelJS.Workbook();
+ const worksheet = workbook.addWorksheet("Sheet1");
+ worksheet.addRow([1, 2, 3]);
+
+ return (await workbook.xlsx.writeBuffer()) as ArrayBuffer;
+}
+
// exceljsのCell.valueは文字列・数値・日付・数式結果オブジェクトなど
// 型がまちまちなので、表示用の1本の文字列に正規化する
function cellToText(value: ExcelJS.CellValue): string {
From a57b687537db54a37057a63146fca359cb917d71 Mon Sep 17 00:00:00 2001
From: "R.H" <312978554+6756432598-creator@users.noreply.github.com>
Date: Fri, 18 Sep 2026 19:18:44 +0900
Subject: [PATCH 2/4] =?UTF-8?q?=E3=83=86=E3=83=B3=E3=83=97=E3=83=AC?=
=?UTF-8?q?=E3=83=BC=E3=83=88=E3=81=A8=E5=8D=98=E8=AA=9E=E8=BF=BD=E5=8A=A0?=
=?UTF-8?q?=E6=A9=9F=E8=83=BD=E4=BF=AE=E6=AD=A3=E7=89=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/my-notebooks/page.tsx | 28 +++----
app/my-notebooks/template/route.ts | 110 ++++++++++++++++++++++---
components/my-notebooks/ImportForm.tsx | 22 ++---
3 files changed, 119 insertions(+), 41 deletions(-)
diff --git a/app/my-notebooks/page.tsx b/app/my-notebooks/page.tsx
index 87e589b..1363cbf 100644
--- a/app/my-notebooks/page.tsx
+++ b/app/my-notebooks/page.tsx
@@ -1,7 +1,7 @@
import Link from "next/link";
import { prisma } from "@/lib/prisma";
-import ImportForm, {ImportTemplate,} from "@/components/my-notebooks/ImportForm";
+import ImportForm, { ImportTemplate } from "@/components/my-notebooks/ImportForm";
import DeleteNotebookButton from "@/components/my-notebooks/DeleteNotebookButton";
import StarColorSettings from "@/components/StarColorSettings";
@@ -32,21 +32,19 @@ export default async function MyNotebooksPage() {
{/*以下の2つのセクションをパソコンでは横並びで、スマホではたて並びで表示する*/}
+
-
-
-
-
+
diff --git a/app/my-notebooks/template/route.ts b/app/my-notebooks/template/route.ts
index 7d66d5e..cd37530 100644
--- a/app/my-notebooks/template/route.ts
+++ b/app/my-notebooks/template/route.ts
@@ -6,17 +6,97 @@ export async function GET(request: NextRequest) {
const filename = searchParams.get("filename") || "単語帳テンプレート";
const language = searchParams.get("language") || "英語";
-const languageMap: Record = {
- 英語: { col1: "単語", col2: "意味", col3: "", col4: "", col5: "", col6: "", col1_2: "example", col2_2: "例", col3_2: "", col4_2: "", col5_2: "", col6_2: "" },
- ドイツ語: { col1: "単語", col2: "意味", col3: "性", col4: "複数形", col5: "格・前置詞", col6: "", col1_2: "Beispiel", col2_2: "例", col3_2: "das Beispiel", col4_2: "die Beispiele", col5_2: "", col6_2: "" },
- 中国語: { col1: "単語", col2: "意味", col3: "拼音", col4: "", col5: "", col6: "", col1_2: "例子", col2_2: "例", col3_2: "lìzi", col4_2: "", col5_2: "", col6_2: "" },
- フランス語: { col1: "単語", col2: "意味", col3: "性", col4: "格・前置詞", col5: "", col6: "", col1_2: "exemple", col2_2: "例", col3_2: "un exemple", col4_2: "", col5_2: "", col6_2: "" },
- スペイン語: { col1: "単語", col2: "意味", col3: "性", col4: "格・前置詞", col5: "", col6: "", col1_2: "ejemplo", col2_2: "例", col3_2: "el ejemplo", col4_2: "", col5_2: "", col6_2: "" },
+ const languageMap: Record<
+ string,
+ {
+ col1: string;
+ col2: string;
+ col3: string;
+ col4: string;
+ col5: string;
+ col6: string;
+ col1_2: string;
+ col2_2: string;
+ col3_2: string;
+ col4_2: string;
+ col5_2: string;
+ col6_2: string;
+ }
+ > = {
+ 英語: {
+ col1: "単語",
+ col2: "意味",
+ col3: "",
+ col4: "",
+ col5: "",
+ col6: "",
+ col1_2: "example",
+ col2_2: "例",
+ col3_2: "",
+ col4_2: "",
+ col5_2: "",
+ col6_2: "",
+ },
+ ドイツ語: {
+ col1: "単語",
+ col2: "意味",
+ col3: "性",
+ col4: "複数形",
+ col5: "格・前置詞",
+ col6: "",
+ col1_2: "Beispiel",
+ col2_2: "例",
+ col3_2: "das Beispiel",
+ col4_2: "die Beispiele",
+ col5_2: "",
+ col6_2: "",
+ },
+ 中国語: {
+ col1: "単語",
+ col2: "意味",
+ col3: "拼音",
+ col4: "",
+ col5: "",
+ col6: "",
+ col1_2: "例子",
+ col2_2: "例",
+ col3_2: "lìzi",
+ col4_2: "",
+ col5_2: "",
+ col6_2: "",
+ },
+ フランス語: {
+ col1: "単語",
+ col2: "意味",
+ col3: "性",
+ col4: "格・前置詞",
+ col5: "",
+ col6: "",
+ col1_2: "exemple",
+ col2_2: "例",
+ col3_2: "un exemple",
+ col4_2: "",
+ col5_2: "",
+ col6_2: "",
+ },
+ スペイン語: {
+ col1: "単語",
+ col2: "意味",
+ col3: "性",
+ col4: "格・前置詞",
+ col5: "",
+ col6: "",
+ col1_2: "ejemplo",
+ col2_2: "例",
+ col3_2: "el ejemplo",
+ col4_2: "",
+ col5_2: "",
+ col6_2: "",
+ },
};
const template = languageMap[language] ?? languageMap["英語"];
-
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("Sheet1");
@@ -26,10 +106,17 @@ const languageMap: Record
-
-
-
- 言語を選択
-
+
+
言語を選択
{languageOptions.map((language) => {
const isSelected = selectedLanguage === language;
@@ -88,7 +83,7 @@ export function ImportTemplate() {
-