diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index df3f424..dc0c6b5 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -7,7 +7,7 @@ on:
jobs:
build:
- name: Node.js ubuntu-latest 14.x
+ name: Node.js ubuntu-latest 20.x
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
@@ -16,7 +16,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
- node-version: 16
+ node-version: 20
- name: before cache
run: |
diff --git a/package.json b/package.json
index d719070..69eee0b 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"next": "13.5.6",
"react": "^18",
"react-dom": "^18",
+ "react-hook-form": "^7.49.2",
"rehype-sanitize": "^6.0.0",
"sass": "^1.69.5",
"url-join": "^5.0.0"
diff --git a/src/app/[slug]/[date]/page.tsx b/src/app/[slug]/[date]/page.tsx
index 8f17377..8998c0b 100644
--- a/src/app/[slug]/[date]/page.tsx
+++ b/src/app/[slug]/[date]/page.tsx
@@ -1,7 +1,7 @@
import { Metadata } from 'next';
import { NippoBreadcrumbs } from './_components/NippoBreadcrumbs';
import { getNippoByDate } from '~/app/_actions/nippoActions';
-import { getObjectiveBySlug } from '~/app/_actions/objectiveActions';
+import { getObjectiveBySlug, getObjectiveTasks } from '~/app/_actions/objectiveActions';
import { URLS } from '~/app/_constants/urls';
import { generateNippoMetadata } from '~/libs/generateNippoMetadata';
import { fetchMe } from '~/app/_actions/userActions';
@@ -9,6 +9,7 @@ import { NippoEditor } from '~/app/_components/domains/Nippo/NippoEditor';
import { getDateString } from '~/libs/getDateString';
import { NippoPreview } from '~/app/_components/domains/Nippo/NippoPreview';
import { NippoShareIcon } from '~/app/_components/domains/Nippo/NippoShareIcon';
+import { TaskList } from '~/app/_components/domains/Task/TaskList';
type Props = { params: { slug: string; date: string } };
@@ -25,6 +26,7 @@ export default async function Page({ params }: Props) {
getNippoByDate(params.slug, params.date),
fetchMe(),
]);
+ const { result: tasksPagination } = await getObjectiveTasks({ objectiveId: objective._id, page: 1 });
const dateString = getDateString(params.date);
return (
@@ -32,15 +34,25 @@ export default async function Page({ params }: Props) {
-
-
{dateString}
- {nippo &&
}
+
+
+
+
{dateString}
+ {nippo &&
}
+
+ {currentUser?._id === objective.createdUserId ? (
+
+ ) : (
+
+ )}
+
+ {/* TODO:一旦非表示 */}
+ {false && (
+
+
+
+ )}
- {currentUser?._id === objective.createdUserId ? (
-
- ) : (
-
- )}
diff --git a/src/app/_actions/objectiveActions.ts b/src/app/_actions/objectiveActions.ts
index f46446f..6fb29ff 100644
--- a/src/app/_actions/objectiveActions.ts
+++ b/src/app/_actions/objectiveActions.ts
@@ -1,12 +1,20 @@
'use server';
import { redirect } from 'next/navigation';
-import { API_OBJECTIVE, API_OBJECTIVE_ID, API_OBJECTIVE_ID_NIPPO, API_OBJECTIVE_ME, API_OBJECTIVE_SLUG } from '../_constants/apiUrls';
+import {
+ API_OBJECTIVE,
+ API_OBJECTIVE_ID,
+ API_OBJECTIVE_ID_NIPPO,
+ API_OBJECTIVE_ID_TASK,
+ API_OBJECTIVE_ME,
+ API_OBJECTIVE_SLUG,
+} from '../_constants/apiUrls';
import { URLS } from '../_constants/urls';
import { Objective } from '~/domains/Objective';
import { apiGet, apiPost } from '~/libs/apiClient';
import { Nippo } from '~/domains/Nippo';
import { PaginationResult } from '~/domains/PaginationResult';
+import { Task } from '~/domains/Task';
export const getObjectiveMe = async () => {
return await apiGet<{ objective: Objective }>(API_OBJECTIVE_ME());
@@ -47,3 +55,9 @@ export const getObjectiveNippos = async ({
next: isMyObjective ? undefined : { revalidate: 60 },
});
};
+
+export const getObjectiveTasks = async ({ objectiveId, page }: { objectiveId: string; page: number }) => {
+ return await apiGet<{ result: PaginationResult }>(`${API_OBJECTIVE_ID_TASK(objectiveId)}?page=${page}`, {
+ cache: 'no-store',
+ });
+};
diff --git a/src/app/_actions/taskActions.ts b/src/app/_actions/taskActions.ts
new file mode 100644
index 0000000..a4f81fc
--- /dev/null
+++ b/src/app/_actions/taskActions.ts
@@ -0,0 +1,26 @@
+'use server';
+
+import { API_TASK } from '../_constants/apiUrls';
+import { apiPost } from '~/libs/apiClient';
+import { Task } from '~/domains/Task';
+
+export const postTask = async ({
+ body,
+ title,
+ dueDate,
+ objectiveId,
+}: {
+ body: string;
+ title: string;
+ dueDate: Date;
+ objectiveId: string;
+}) => {
+ return await apiPost<{ task: Task }>(API_TASK(), {
+ body: JSON.stringify({
+ body,
+ title,
+ dueDate,
+ objectiveId,
+ }),
+ });
+};
diff --git a/src/app/_components/domains/Nippo/NippoEditor/NippoEditor.tsx b/src/app/_components/domains/Nippo/NippoEditor/NippoEditor.tsx
index a2815a8..f88f9c9 100644
--- a/src/app/_components/domains/Nippo/NippoEditor/NippoEditor.tsx
+++ b/src/app/_components/domains/Nippo/NippoEditor/NippoEditor.tsx
@@ -1,19 +1,9 @@
'use client';
-import './styles.scss';
-
-import { Color } from '@tiptap/extension-color';
-import { Link } from '@tiptap/extension-link';
-import { useEffect, useState, FC, useCallback } from 'react';
-import { EditorContent, useEditor } from '@tiptap/react';
-import TextStyle from '@tiptap/extension-text-style';
-import Placeholder from '@tiptap/extension-placeholder';
-import StarterKit from '@tiptap/starter-kit';
-import ListItem from '@tiptap/extension-list-item';
-import Heading from '@tiptap/extension-heading';
+import { FC, useCallback } from 'react';
import { postNippo } from '~/app/_actions/nippoActions';
import { Nippo } from '~/domains/Nippo';
-import { useDebounce } from '~/libs/useDebounce';
+import { DebounceEditor } from '~/app/_components/uiParts/Editor/DebounceEditor';
type Props = {
objectiveId: string;
@@ -22,9 +12,6 @@ type Props = {
};
export const NippoEditor: FC = ({ objectiveId, nippo, date }) => {
- const [inputText, setInputText] = useState();
- const debouncedInputText = useDebounce({ value: inputText, delay: 200 });
-
const handleEditorChange = useCallback(
async (body: string) => {
await postNippo({ objectiveId, date, body });
@@ -32,44 +19,5 @@ export const NippoEditor: FC = ({ objectiveId, nippo, date }) => {
[date, objectiveId],
);
- useEffect(() => {
- debouncedInputText && handleEditorChange(debouncedInputText);
- }, [debouncedInputText, handleEditorChange]);
-
- const extensions = [
- Color.configure({ types: [TextStyle.name, ListItem.name] }),
- Link.configure(),
- StarterKit.configure({
- bulletList: {
- keepMarks: true,
- keepAttributes: false,
- },
- orderedList: {
- keepMarks: true,
- keepAttributes: false,
- },
- }),
- Placeholder.configure({
- placeholder: '振り返りを記入しましょう!',
- }),
- Heading.configure({
- levels: [1, 2, 3, 4, 5],
- }),
- ];
-
- const editor = useEditor({
- extensions,
- content: nippo?.body,
- autofocus: 'end',
- onUpdate: ({ editor }) => {
- setInputText(editor.getHTML());
- },
- editorProps: {
- attributes: {
- class: 'min-h-[400px]',
- },
- },
- });
-
- return ;
+ return ;
};
diff --git a/src/app/_components/domains/Task/CreateTaskModal/CreateTaskModal.tsx b/src/app/_components/domains/Task/CreateTaskModal/CreateTaskModal.tsx
new file mode 100644
index 0000000..2edd8ec
--- /dev/null
+++ b/src/app/_components/domains/Task/CreateTaskModal/CreateTaskModal.tsx
@@ -0,0 +1,88 @@
+'use client';
+
+import { FC, useCallback, useState } from 'react';
+import { Button, Input, Modal, ModalBody, ModalContent, ModalFooter } from '@nextui-org/react';
+import { Controller, SubmitHandler, useForm } from 'react-hook-form';
+import { TaskEditor } from '../TaskEditor';
+import { postTask } from '~/app/_actions/taskActions';
+
+type Props = {
+ isOpen: boolean;
+ onOpenChange: () => void;
+ objectiveId: string;
+};
+
+interface IFormInput {
+ title: string;
+ body: string;
+}
+
+export const CreateModal: FC = ({ isOpen, onOpenChange, objectiveId }) => {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { control, formState, handleSubmit, reset } = useForm({
+ defaultValues: {
+ title: '',
+ body: '',
+ },
+ });
+
+ const handleOpenChange = useCallback(() => {
+ reset();
+ onOpenChange();
+ }, [onOpenChange, reset]);
+
+ const onSubmit: SubmitHandler = useCallback(
+ async (inputData) => {
+ if (isLoading) return;
+ setIsLoading(true);
+ postTask({ title: inputData.title, body: inputData.body, dueDate: new Date(), objectiveId })
+ .catch((error) => {
+ // TODO: 本来はコンソールに出すのではなく、ユーザーにエラーを通知する
+ console.error(error);
+ })
+ .then(() => {
+ handleOpenChange();
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ },
+ [handleOpenChange, isLoading, objectiveId],
+ );
+
+ return (
+
+
+
+ (
+
+ )}
+ />
+ field.onChange(body)} />}
+ />
+
+
+
+
+
+
+ );
+};
diff --git a/src/app/_components/domains/Task/CreateTaskModal/index.ts b/src/app/_components/domains/Task/CreateTaskModal/index.ts
new file mode 100644
index 0000000..15d0f3c
--- /dev/null
+++ b/src/app/_components/domains/Task/CreateTaskModal/index.ts
@@ -0,0 +1 @@
+export { CreateModal } from './CreateTaskModal';
diff --git a/src/app/_components/domains/Task/TaskEditor/TaskEditor.tsx b/src/app/_components/domains/Task/TaskEditor/TaskEditor.tsx
new file mode 100644
index 0000000..7185638
--- /dev/null
+++ b/src/app/_components/domains/Task/TaskEditor/TaskEditor.tsx
@@ -0,0 +1,12 @@
+'use client';
+
+import { FC } from 'react';
+import { Editor } from '~/app/_components/uiParts/Editor';
+
+type Props = {
+ onChangeText: (body: string) => void;
+};
+
+export const TaskEditor: FC = ({ onChangeText }) => {
+ return ;
+};
diff --git a/src/app/_components/domains/Task/TaskEditor/index.ts b/src/app/_components/domains/Task/TaskEditor/index.ts
new file mode 100644
index 0000000..f83b3d0
--- /dev/null
+++ b/src/app/_components/domains/Task/TaskEditor/index.ts
@@ -0,0 +1 @@
+export { TaskEditor } from './TaskEditor';
diff --git a/src/app/_components/domains/Task/TaskList/TaskList.tsx b/src/app/_components/domains/Task/TaskList/TaskList.tsx
new file mode 100644
index 0000000..1e6d5b8
--- /dev/null
+++ b/src/app/_components/domains/Task/TaskList/TaskList.tsx
@@ -0,0 +1,35 @@
+'use client';
+
+import { Button, Card, useDisclosure } from '@nextui-org/react';
+import { FC } from 'react';
+import { CreateModal } from '../CreateTaskModal';
+import { Icon } from '~/app/_components/uiParts/icons';
+import { Task } from '~/domains/Task';
+
+type Props = {
+ objectiveId: string;
+ tasks: Task[];
+};
+
+export const TaskList: FC = ({ objectiveId, tasks }) => {
+ const { isOpen, onOpen, onOpenChange } = useDisclosure();
+
+ return (
+
+
タスク一覧
+
+ {tasks.map((task) => {
+ return (
+
+ ここにタスク一覧が表示されます
+
+ );
+ })}
+
+
} onClick={() => onOpen()}>
+ タスクの追加
+
+
+
+ );
+};
diff --git a/src/app/_components/domains/Task/TaskList/index.ts b/src/app/_components/domains/Task/TaskList/index.ts
new file mode 100644
index 0000000..19a80e9
--- /dev/null
+++ b/src/app/_components/domains/Task/TaskList/index.ts
@@ -0,0 +1 @@
+export { TaskList } from './TaskList';
diff --git a/src/app/_components/uiParts/Editor/DebounceEditor.tsx b/src/app/_components/uiParts/Editor/DebounceEditor.tsx
new file mode 100644
index 0000000..ae7b14b
--- /dev/null
+++ b/src/app/_components/uiParts/Editor/DebounceEditor.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+import './styles.scss';
+
+import { useEffect, useState, FC, useCallback } from 'react';
+import { Editor } from './Editor';
+import { useDebounce } from '~/libs/useDebounce';
+
+type Props = {
+ body?: string;
+ placeholder: string;
+ onChange: (body: string) => Promise;
+};
+
+export const DebounceEditor: FC = ({ body, placeholder, onChange }) => {
+ const [inputText, setInputText] = useState();
+ const debouncedInputText = useDebounce({ value: inputText, delay: 200 });
+
+ const handleEditorChange = useCallback(
+ async (body: string) => {
+ await onChange(body);
+ },
+ [onChange],
+ );
+
+ useEffect(() => {
+ debouncedInputText && handleEditorChange(debouncedInputText);
+ }, [debouncedInputText, handleEditorChange]);
+
+ return setInputText(body)} />;
+};
diff --git a/src/app/_components/uiParts/Editor/Editor.tsx b/src/app/_components/uiParts/Editor/Editor.tsx
new file mode 100644
index 0000000..fab84e9
--- /dev/null
+++ b/src/app/_components/uiParts/Editor/Editor.tsx
@@ -0,0 +1,58 @@
+'use client';
+
+import './styles.scss';
+
+import { Color } from '@tiptap/extension-color';
+import { Link } from '@tiptap/extension-link';
+import { FC } from 'react';
+import { EditorContent, useEditor } from '@tiptap/react';
+import TextStyle from '@tiptap/extension-text-style';
+import Placeholder from '@tiptap/extension-placeholder';
+import StarterKit from '@tiptap/starter-kit';
+import ListItem from '@tiptap/extension-list-item';
+import Heading from '@tiptap/extension-heading';
+
+type Props = {
+ body?: string;
+ placeholder: string;
+ onChange: (body: string) => void;
+};
+
+export const Editor: FC = ({ body, placeholder, onChange }) => {
+ const extensions = [
+ Color.configure({ types: [TextStyle.name, ListItem.name] }),
+ Link.configure(),
+ StarterKit.configure({
+ bulletList: {
+ keepMarks: true,
+ keepAttributes: false,
+ },
+ orderedList: {
+ keepMarks: true,
+ keepAttributes: false,
+ },
+ }),
+ Placeholder.configure({
+ placeholder,
+ }),
+ Heading.configure({
+ levels: [1, 2, 3, 4, 5],
+ }),
+ ];
+
+ const editor = useEditor({
+ extensions,
+ content: body,
+ autofocus: 'end',
+ onUpdate: ({ editor }) => {
+ onChange(editor.getHTML());
+ },
+ editorProps: {
+ attributes: {
+ class: 'min-h-[400px]',
+ },
+ },
+ });
+
+ return ;
+};
diff --git a/src/app/_components/uiParts/Editor/index.ts b/src/app/_components/uiParts/Editor/index.ts
new file mode 100644
index 0000000..5ccde0e
--- /dev/null
+++ b/src/app/_components/uiParts/Editor/index.ts
@@ -0,0 +1 @@
+export { Editor } from './Editor';
diff --git a/src/app/_components/domains/Nippo/NippoEditor/styles.scss b/src/app/_components/uiParts/Editor/styles.scss
similarity index 100%
rename from src/app/_components/domains/Nippo/NippoEditor/styles.scss
rename to src/app/_components/uiParts/Editor/styles.scss
diff --git a/src/app/_constants/apiUrls.ts b/src/app/_constants/apiUrls.ts
index 23c6b6a..bcf0bcc 100644
--- a/src/app/_constants/apiUrls.ts
+++ b/src/app/_constants/apiUrls.ts
@@ -8,3 +8,7 @@ export const API_OBJECTIVE_ME = () => `/api/objectives/me`;
export const API_OBJECTIVE_ID_NIPPO = (_id: string) => `/api/objectives/${_id}/nippos`;
export const API_NIPPO_BY_DATE = (slug: string, data: string) => `/api/nippos/by-date?date=${data}&slug=${slug}`;
+
+export const API_OBJECTIVE_ID_TASK = (_id: string) => `/api/objectives/${_id}/tasks`;
+export const API_TASK = () => `/api/tasks`;
+export const API_TASK_ID = (id: string) => `/api/tasks/${id}`;
diff --git a/src/domains/Task/Task.ts b/src/domains/Task/Task.ts
new file mode 100644
index 0000000..5e62e6b
--- /dev/null
+++ b/src/domains/Task/Task.ts
@@ -0,0 +1,10 @@
+export interface Task {
+ _id: string;
+ title: string;
+ body: string;
+ createdUserId: string;
+ objectiveId: string;
+ dueDate: string; // 対応日時
+ createdAt: string;
+ updatedAt: string;
+}
diff --git a/src/domains/Task/index.ts b/src/domains/Task/index.ts
new file mode 100644
index 0000000..9fc71d8
--- /dev/null
+++ b/src/domains/Task/index.ts
@@ -0,0 +1 @@
+export type { Task } from './Task';
diff --git a/yarn.lock b/yarn.lock
index b742930..d6d9a00 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4775,6 +4775,11 @@ react-dom@^18:
loose-envify "^1.1.0"
scheduler "^0.23.0"
+react-hook-form@^7.49.2:
+ version "7.49.2"
+ resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.49.2.tgz#6fb2742e1308020f26cb1915c7012b6c07b11ade"
+ integrity sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==
+
react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"