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つのセクションをパソコンでは横並びで、スマホではたて並びで表示する*/}
+
+

+ 自分のExcelから新規作成 +

+ +
-
-

- 自分のExcelから新規作成 -

- -
- -
-

- テンプレートから新規作成 -

- -
- +
+

+ テンプレートから新規作成 +

+ +
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() {
-