Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix#419 下書き機能 #448

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
95 changes: 95 additions & 0 deletions src/app/committee/news/new/NewNewsForm.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
.draftsWrap {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 20px;
background-color: #f4f4f4;
padding: 20px 36px;
gap: 20px;
border-radius: 16px;
align-items: stretch;
}
.draftsWrap .display {
display: flex;
align-items: start;
flex-direction: column;
gap: 12px;
line-height: 1.4;
}
.draftsWrap .display .statusWrap {
display: flex;
gap: 6px;
align-items: center;
font-size: 22px;
line-height: 1;
color: #000;
font-weight: 800;
margin-top: 6px;
}
.draftsWrap .display .statusWrap .statusIcon {
width: 22px;
height: 22px;
}
.draftsWrap .display .note {
color: #a59e9e;
font-size: 15px;
}
.draftsWrap .actions {
display: flex;
align-items: center;
gap: 4px;
}
.draftsWrap .actions .btn {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
max-height: 50px;
background-color: #fff;
border: 2px solid #eee;
font-size: 15px;
border-radius: 8px;
transition: 0.2s;
gap: 4px;
font-weight: 600;
width: 50px;
cursor: pointer;
}
.draftsWrap .actions .btn:hover {
background-color: #f4f4f4;
border: 2px solid #dfdfdf;
}
.draftsWrap .actions .btn__icon {
width: 20px;
height: 20px;
}
.draftsWrap .actions .selectsWrap {
position: relative;
height: 100%;
max-height: 50px;
}
.draftsWrap .actions .selectsWrap .selects {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
background-color: #fff;
border: 2px solid #eee;
font-size: 15px;
border-radius: 8px;
transition: 0.2s;
padding: 0 50px 0 20px;
-webkit-appearance: none;
appearance: none;
}
.draftsWrap .actions .selectsWrap .selects:hover {
background-color: #f4f4f4;
border: 2px solid #dfdfdf;
}
.draftsWrap .actions .selectsWrap .arrow {
position: absolute;
top: calc(50% - 6px);
right: 20px;
width: 12px;
height: 12px;
}
203 changes: 195 additions & 8 deletions src/app/committee/news/new/NewNewsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { css } from "@styled-system/css";
import { Button } from "@/common_components/Button";
import Image from "next/image";
import sendIcon from "@/assets/Send.svg?url";
import { NewNewsSchema, NewNewsSchemaType, projectAttributes, projectCategories } from "@/lib/valibot";
import { NewNewsSchema, NewNewsSchemaType, projectAttributes, projectCategories, ProjectCategory } from "@/lib/valibot";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { valibotResolver } from "@hookform/resolvers/valibot";
Expand All @@ -13,12 +13,33 @@ import toast from "react-hot-toast";
import { ProjectCategorySelector } from "@/common_components/ProjectCategorySelector";
import { TitleField } from "@/common_components/news/TitleField";
import { BodyField } from "@/common_components/news/BodyField";
import { useState } from "react";
import { useEffect, useState, FC } from "react";
import { FileErrorsType } from "@/common_components/form_answer/FormItems";
import { FilesField } from "@/common_components/form_editor/FilesEditor";
import { filesStatus } from "@/common_components/form_editor/FilesInterfaces";
import pageStyle from "./NewNewsForm.module.css";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
import "dayjs/locale/ja";
dayjs.locale("ja");

export const NewNewsForm = () => {
import CheckIcon from "./_checkIcon.svg?url";
import TrashIcon from "./_trashBtnIcon.svg?url";
import SelectArrow from "./_optionsArrow.svg?url";

interface NewsProps {
uid: string;
title: string;
body: string;
categories: string[];
attachments: filesStatus[];
updatedAt: string;
}

export const NewNewsForm: FC<{
draft: string | null;
}> = ({ draft }) => {
const router = useRouter();
const {
register,
Expand All @@ -32,6 +53,54 @@ export const NewNewsForm = () => {
const [fileErrors, setFileErrors] = useState<FileErrorsType>(new Map([["attachments", null]]));
type FileIds = { [itemId: string]: string[] };

const [drafts, setDrafts] = useState<NewsProps[] | null>(null);
const [drafts_loaded, setDraftsLoaded] = useState<boolean>(false);
useEffect(() => {
const item: string | null = localStorage.getItem("sos_news_drafts");
if (item) {
setDrafts(JSON.parse(item) ?? null);
if (draft) {
JSON.parse(item).forEach((draft_items: NewsProps) => {
if (draft_items?.uid === draft) {
// console.log("Found draft", draft_items);
setTitle(draft_items.title ?? "");
setBody(draft_items.body ?? "");
setCategories((draft_items.categories as ProjectCategory[]) ?? []);
setFilesStatus(draft_items?.attachments ?? []);
}
});
}
setDraftsLoaded(true);
} else {
setDrafts([]);
setDraftsLoaded(true);
}
}, []);
// useEffect(() => {
// if (drafts && drafts.length > 0) {
// console.log("drafts", drafts);
// }
// }, [drafts]);
useEffect(() => {
if (drafts) {
if (drafts_loaded) {
localStorage.setItem("sos_news_drafts", JSON.stringify(drafts));
// console.log("Drafts saved to localStorage", drafts);
// } else {
// console.log("load されていないので保存できません", drafts);
}
}
}, [drafts]);

// const [draftUID, setDraftUID] = useState<string>(Math.random().toString(32).substring(2));
const [draftUID, setDraftUID] = useState<string>(draft ?? Math.random().toString(32).substring(2));
useEffect(() => {
// console.log(`${draftUID} になりました`);
if (draftUID && !draft) {
router.replace(`/committee/news/new/${draftUID}`);
}
}, [draftUID]);

const onSubmit = async (data: NewNewsSchemaType) => {
if (fileErrors.get("attachments")) {
toast.error("添付ファイルを正しく選択してください");
Expand Down Expand Up @@ -65,6 +134,29 @@ export const NewNewsForm = () => {
);
};

const [categories, setCategories] = useState<ProjectCategory[]>([]);
const [title, setTitle] = useState<string>("");
const [body, setBody] = useState<string>("");
useEffect(() => {
updateSome();
}, [categories, title, body, filesStatus]);
const updateSome = () => {
drafts_loaded &&
setDrafts([
...(drafts?.filter((draft) => {
return draft.uid !== draftUID;
}) ?? []),
{
uid: draftUID,
title: title,
body: body,
categories: categories,
attachments: filesStatus,
updatedAt: new Date().toISOString(),
},
]);
};

const onSubmitHandler = handleSubmit(async (data) => {
onSubmit(data);
});
Expand All @@ -80,7 +172,7 @@ export const NewNewsForm = () => {
return onSubmitHandler();
}
}}
className={stack({ gap: 4 })}>
className={stack({ gap: 4, marginBottom: "100px" })}>
<div
className={hstack({
justifyContent: "space-between",
Expand All @@ -104,17 +196,112 @@ export const NewNewsForm = () => {
<Image src={sendIcon} alt="" />
</Button>
</div>
<ProjectCategorySelector register={register("categories")} error={errors.categories?.message} />
<TitleField register={register("title")} error={errors.title?.message} />
<BodyField register={register("body")} error={errors.body?.message} />
{drafts && drafts.length > 0 && (
<div className={pageStyle.draftsWrap}>
<div className={pageStyle.display}>
<div className={pageStyle.statusWrap}>
{drafts_loaded && drafts.find((draft_item: NewsProps) => draft_item.uid === draftUID) ? (
<>
<Image src={CheckIcon} className={pageStyle.statusIcon} alt="" />
<span>ローカルに保存済み</span>
</>
) : (
<span>保存されていません</span>
)}
</div>
<p className={pageStyle.note}>他のブラウザには同期されません</p>
</div>
<div className={pageStyle.actions}>
<button
className={pageStyle.btn}
onClick={() => {
setDrafts(
drafts?.filter((draft_item: NewsProps) => {
return draft_item.uid !== draftUID;
}),
);
router.push(`/committee/news/new`);
}}>
<Image src={TrashIcon} className={pageStyle.btn__icon} alt="" />
</button>
<div className={pageStyle.selectsWrap}>
<select
value={draftUID}
onChange={(e) => {
router.push(`/committee/news/new/${e.target.value}`);
setDraftUID(e.target.value);
}}
className={pageStyle.selects}>
<option value="">新規作成</option>
{drafts?.map((draft_item: NewsProps) => (
<option key={draft_item.uid} value={draft_item.uid} selected={draft_item.uid === draft}>
{draft_item.title !== "" ? draft_item.title : "無題"} -{" "}
{draft_item.updatedAt && dayjs(draft_item.updatedAt).fromNow()}
</option>
))}
</select>
<Image src={SelectArrow} alt="" className={pageStyle.arrow} />
</div>
</div>
</div>
)}
<ProjectCategorySelector
register={register("categories", {
onChange: (e) => {
if (e.target.checked) {
setCategories([...categories, e.target.value]);
// console.log("Add Cat:", e.target.value);
} else {
setCategories(categories.filter((item) => item !== e.target.value));
// console.log("Remove Cat:", e.target.value);
}
updateSome();
},
})}
checkedCategories={categories}
error={errors.categories?.message}
/>
<TitleField
register={register("title", {
onChange: (e) => {
// console.log("Custom onChange Title:", e.target.value);
setTitle(e.target.value);
},
})}
error={errors.title?.message}
value={title}
/>
<BodyField
register={register("body", {
onChange: (e) => {
setBody(e.target.value);
},
})}
error={errors.body?.message}
value={body}
/>
<FilesField
label="添付ファイル"
register={register("attachments")}
register={register(
"attachments",
// {onChange: (e) => {
// // console.log("Custom onChange Attachment:", e.target.value);
// },}
)}
id="attachments"
filesStatus={filesStatus}
setFilesStatus={setFilesStatus}
setErrorState={setFileErrors}
/>
<p
className={css({
color: "gray.500",
fontSize: "sm",
mt: "50px",
})}>
<b>重要なお知らせの下書きは、必ずご自身で管理するそぽ!</b>
下書き機能は便宜のための補助的な機能なので、保存内容の安全性は保証されません。なお、下書きはブラウザのローカルストレージに保存され、他のブラウザやデバイスには同期されません。共有デバイス等信頼できないデバイスでは、必ず「サインアウト」を行ってください。
</p>
</form>
);
};
29 changes: 29 additions & 0 deletions src/app/committee/news/new/[draft_id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

import { container, stack } from "@styled-system/patterns";
import Link from "next/link";
import { css } from "@styled-system/css";
import { NewNewsForm } from "@/app/committee/news/new/NewNewsForm";
import { NextPage } from "next";

export const runtime = "edge";

const NewNewsPage: NextPage<{ params: { draft_id: string } }> = ({ params }) => {
return (
<div className={container({ maxWidth: "4xl" })}>
<div className={stack({ gap: 8, marginY: 8 })}>
<Link
href="/committee/news"
className={css({
color: "tsukuba.purple",
fontSize: "xs",
})}>
←お知らせ一覧に戻る
</Link>
<NewNewsForm draft={params.draft_id} />
</div>
</div>
);
};

export default NewNewsPage;
5 changes: 5 additions & 0 deletions src/app/committee/news/new/_checkIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/app/committee/news/new/_optionsArrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/app/committee/news/new/_trashBtnIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading