AllNotes.tsx
const AllNotes = () => {
const dispatch = useAppDispatch();
const { mainNotes } = useAppSelector((state) => state.notesList);
const [filter, setFilter] = useState('');
const [searchInput, setSearchInput] = useState('');
return (
<Container>
{mainNotes.length === 0 ? (
<EmptyMsgBox>노트가 없습니다.</EmptyMsgBox>
) : (
<>
<TopBox>
<InputBox>
<input
type="text"
value={searchInput}
onChange={ (e) => setSearchInput(e.target.value)}
placeholder="노트의 제목을 입력해주세요."
/>
</InputBox>
<div className='notes__filter-btn'>
<ButtonOutline
onClick={() => dispatch(toggleFiltersModal(true))}
className='nav__btn'
>
<span>정렬</span>
</ButtonOutline>
</div>
</TopBox>
<Box>
{/* Notes */}
</Box>
</>
)}
</Container>
)
}
태그 이니셜데이터를 tagSlice.ts
에 생성해놨었는데
노트도 들어가자마자 있을 수 있게 두개 생성하겠다.
src > notesData.ts
import { v4 } from "uuid";
export interface Note {
title: string;
content: string;
tags: Tag[];
color: string;
priority: string;
isPinned: boolean;
isRead: boolean;
date: string;
createdTime: number;
editedTime: null | number;
id: string;
}
const notes = [
{
title: "Note 1 title",
content: "Note 1 content",
tags: [{ tag: "coding", id: v4() }],
color: "#cce0ff",
priority: "high",
isPinned: true,
isRead: false,
date: "10/12/22 2.55 PM",
createdTime: new Date("Sat Dec 10 2022 14:55:22").getTime(),
editedTime: null,
id: v4()
},
{
title: "Note 2 title",
content: "Note 2 content",
tags: [{ tag: "exercise", id: v4() }],
color: "#ffcccc",
priority: "high",
isPinned: true,
isRead: false,
date: "10/12/22 2.55 PM",
createdTime: new Date("Sat Dec 10 2022 14:55:22").getTime(),
editedTime: null,
id: v4()
}
]
export default notes;
이것을 mainNotes에 넣어야함
src > store > notesListSlice.ts
const initialState: NoteState = {
mainNotes: [...notes],
archiveNotes: [],
trashNotes: [],
editNote: null,
}
utils > getAllNotes.tsx
import { NoteCard } from "../components";
import { NotesContainer } from "../styles/styles";
import { Note } from "../types/note";
const getAllNotes = (mainNotes: Note[], filter: string) => {
return (
<>
<div className="allNotes__notes-type">
All Notes
</div>
<NotesContainer>
{mainNotes.map((note) => (
<NoteCard key={note.id} note={note} type="notes" />
))}
</NotesContainer>
</>
)
}
export default getAllNotes;
components > NoteCard > NoteCard.tsx
import React from 'react'
import { Card, ContentBox, FooterBox, TagsBox, TopBox } from './NoteCard.styles';
import { NotesIconBox } from '../../styles/styles';
import { BsFillPinFill } from 'react-icons/bs';
import { Note } from '../../types/note';
interface NoteCardProps {
note: Note,
type: string
}
const NoteCard = ({ note, type }: NoteCardProps) => {
const { title, content, tags, color, priority, date, isPinned, isRead, id } = note;
return (
<Card style={{ background: color }}>
<TopBox>
<div
className='noteCard__title'>
{ title.length > 10 ? title.slice(0, 10) + "..." : title }
</div>
<div className='noteCArd__top-options'>
<span className='noteCard__priority'>
{priority}
</span>
{type !== "archive" && type !== "trash" && (
<NotesIconBox
className='noteCard__pin'
>
<BsFillPinFill
style={{ color: isPinned ? "red" : ""}}
/>
</NotesIconBox>
)}
</div>
</TopBox>
<ContentBox>
{content}
</ContentBox>
<TagsBox>
{tags.map(({ tag, id }) => (
<span key={id}>{tag}</span>
))}
</TagsBox>
<FooterBox>
<div className='noteCard__date'>{date}</div>
</FooterBox>
</Card>
)
}
export default NoteCard
edit, archive, trash 기능버튼
utils > getRelevantBtns.tsx
import { FaEdit, FaTrash, FaTrashRestore } from "react-icons/fa";
import { NotesIconBox } from "../styles/styles";
import { RiInboxUnarchiveFill } from 'react-icons/ri';
import { Note } from "../types/note";
import { Dispatch } from "@reduxjs/toolkit";
const getRelevantBtns = (type: string, note: Note, dispatch: Dispatch) => {
if(type === "archive") {
return (
<>
<NotesIconBox
onClick={() => dispatch(unArchiveNote(note))}
data-info="UnArchive"
>
<RiInboxUnarchiveFill style={{ fontSize: '1rem' }} />
</NotesIconBox>
<NotesIconBox
onClick={() => dispatch(setTrashNotes(note))}
data-info="Delete"
>
<FaTrash style={{ fontSize: '1rem' }} />
</NotesIconBox>
</>
)
} else if (type === "trash") {
return (
<>
<NotesIconBox
onClick={() => dispatch(restoreNote(note))}
data-info="Restore"
>
<FaTrashRestore style={{ fontSize: "1rem" }} />
</NotesIconBox>
<NotesIconBox
onClick={() => dispatch(deleteNote(note))}
data-info="Delete"
>
<FaTrash style={{ fontSize: "1rem" }} />
</NotesIconBox>
</>
);
} else {
return (
<>
<NotesIconBox
onClick={clickHandler}
data-info="Edit"
>
<FaEdit style={{ fontSize: "1rem" }} />
</NotesIconBox>
<NotesIconBox
onClick={() => dispatch(setArchiveNotes(note))}
data-info="Archive"
>
<FaTrashRestore style={{ fontSize: "1rem" }} />
</NotesIconBox>
<NotesIconBox
onClick={() => dispatch(setTrashNotes(note))}
data-info="Delete"
>
<FaTrash style={{ fontSize: "1rem" }} />
</NotesIconBox>
</>
);
}
}
export default getRelevantBtns;
src > store > notesListSlice.ts
import { createSlice } from '@reduxjs/toolkit';
import { Note } from '../../types/note';
import notes from '../../notesData';
interface NoteState {
mainNotes: Note[],
archiveNotes: Note[],
trashNotes: Note[],
editNote: null | Note
}
const initialState: NoteState = {
mainNotes: [...notes],
archiveNotes: [],
trashNotes: [],
editNote: null,
}
enum noteType {
mainNotes = 'mainNotes',
archiveNotes = 'archiveNotes',
trashNotes = 'trashNotes'
}
const notesListSlice = createSlice({
name: "notesList",
initialState,
reducers: {
setMainNotes: (state, { payload }) => { // note생성이나 수정 후 업데이트
// 해당 note edit
if(state.mainNotes.find(({id}) => id === payload.id)) { // id가 payload로 들어오는 새로운 노트의 id가 같다면 수정하는데
state.mainNotes = state.mainNotes.map((note) =>
note.id === payload.id ? payload : note) // 같은건 변경해주고 원래있는건 원래있는걸로 넣어줌
}
// note를 새롭게 생성
else {
state.mainNotes.push(payload)
}
},
setTrashNotes: (state, { payload }) => {
state.mainNotes = state.mainNotes.filter(({id})=> id !== payload.id); // payload와 다른것만 넣어줌.
state.archiveNotes = state.archiveNotes.filter(({id})=> id !== payload.id); // payload와 다른것만 넣어줌.
state.trashNotes.push({...payload, isPinned: false}); // 다른 property override해서 ... 풀어서 넣어줌
},
setArchiveNotes: (state, { payload }) => {
state.mainNotes = state.mainNotes.filter(({ id }) => id !== payload.id);
state.archiveNotes.push({...payload, isPinned: false});
},
unArchiveNote: (state, { payload }) => {
state.archiveNotes = state.archiveNotes.filter(({id})=> id !== payload.id);
state.mainNotes.push(payload);
},
restoreNote: (state, { payload }) => {
state.trashNotes = state.trashNotes.filter(({id}) => id !== payload.id);
state.mainNotes.push(payload);
},
deleteNote: (state, { payload }) => {
state.trashNotes = state.trashNotes.filter(({ id }) => id !== payload.id);
},
setPinnedNotes: (state, { payload }) => {
state.mainNotes = state.mainNotes.map((note) =>
note.id === payload.id ? {...note, isPinned: !note.isPinned} : note); // isPinned 값 toggle
},
setEditNote: (state, { payload }) => {
state.editNote = payload;
},
readNote: (state, { payload }) => {
const {type, id} = payload;
const setRead = (notes: noteType) => {
state[notes] = state[notes].map((note: Note) =>
note.id === id ? {...note, isRead: !note.isRead} : note)
}
if(type === "archive") {
setRead(noteType.archiveNotes);
} else if(type === "trash") {
setRead(noteType.trashNotes);
} else { // main, tag...
setRead(noteType.mainNotes);
}
},
removeTags: (state, { payload }) => {
state.mainNotes = state.mainNotes.map((note) => ({
...note, // spread operate
tags: note.tags.filter(({ tag }) => tag !== payload.tag), // payload에 같은 태그는 삭제하고 넣어준다
}));
},
},
});
export const {
setMainNotes,
setArchiveNotes,
setTrashNotes,
unArchiveNote,
restoreNote,
deleteNote,
setPinnedNotes,
setEditNote,
readNote,
removeTags,
} = notesListSlice.actions;
export default notesListSlice.reducer
readNote기능은 archiveNotes 타입이거나, trashNotes, mainNotes 일때 각각 isRead를 toggle해주는 기능이다. true일때 modal이 떠서 조회가 가능하다.
enum noteType을 따로 생성해서 노트타입으로 각 노트리스트에서 해당 노트를 조회하도록 한다.
// modal
const clickHandler = () => {
dispatch(toggleCreateNoteModal(true));
dispatch(setEditNote(note));
};
App.tsx
에서 viewEditTagsModal와 같은 위치에 수정/생성용 모달을 넣는다.
...
const { viewCreateNoteModal } = useAppSelector((state) => state.modal);
...
{viewCreateNoteModal && <CreateNoteModal />}
NoteCard.tsx
에서 onClick에 setPinnedNotes 기능 (noteListSlice.ts에 정의)
{type !== "archive" && type !== "trash" && (
<NotesIconBox
className='noteCard__pin'
onClick={() => dispatch(setPinnedNotes({id}))}
>
<BsFillPinFill
style={{ color: isPinned ? "red" : ""}}
/>
</NotesIconBox>
)}
getRelevantBtns
에서 edit눌렀을때 clickHandler가 동작하면서 모달띄운다.
이때 모달 생성하기
components > Modal > CreateNoteModal.tsx
...
const CreateNoteModal = () => {
const dispatch = useAppDispatch();
const { editNote } = useAppSelector((state) => state.notesList);
const { viewAddTagsModal } = useAppSelector((state) => state.modal);
const [noteTitle, setNoteTitle] = useState(editNote?.title || "");
const [value, setValue] = useState(editNote?.content || "");
const [addedTags, setAddedTags] = useState(editNote?.tags || []);
const [noteColor, setNoteColor] = useState(editNote?.color || "");
const [priority, setPriority] = useState(editNote?.priority || "low");
const closeCreateNoteModal = () => {
dispatch(toggleCreateNoteModal(false));
dispatch(setEditNote(null));
};
const tagsHandler = (tag: string, type: string) => {
// tag add, delete 기능
const newTag = tag.toLowerCase();
if(type === 'add') {
setAddedTags((prev) => [...prev, {tag: newTag, id: v4()}])
console.log("여기2");
} else {
setAddedTags(addedTags.filter(({tag}) => tag !== newTag));
console.log("여기")
}
};
return (
<FixedContainer>
{viewAddTagsModal && (
<TagsModal type="add" addedTags={addedTags} handleTags={tagsHandler} />
)}
<Box>
<TopBox>
<div className="createNote__title">
{editNote ? "노트 수정하기" : "노트 생성하기"}
</div>
<DeleteBox
className="createNote__close-btn"
onClick={closeCreateNoteModal}
></DeleteBox>
</TopBox>
<StyledInput
type="text"
value={noteTitle}
name="title"
placeholder="제목..."
onChange={(e) => setNoteTitle(e.target.value)}
/>
<div className="createNote__create-btn">
<ButtonFill>
{editNote ? (
<span>저장하기</span>
) : (
<>
<FaPlus />
<span>생성하기</span>
</>
)}
</ButtonFill>
</div>
<AddedTagsBox>
{addedTags.map(({ tag, id }) => (
<div key={id}>
<span className="createNote__Tag">{tag}</span>
<span
className="createNote__tag-remove"
onClick={() => tagsHandler(tag, "remove")}
>
<FaTimes />
</span>
</div>
))}
</AddedTagsBox>
<OptionsBox>
<ButtonOutline
onClick={() =>
dispatch(toggleTagsModal({ type: "add", view: "true" }))
}
>
Add Tag
</ButtonOutline>
<div>
<label>배경색 : </label>
<select
value={noteColor}
id="color"
onChange={(e) => setNoteColor(e.target.value)}
>
<option value="">White</option>
<option value="#ffcccc">Red</option>
<option value="#ccffcc">Green</option>
<option value="#cce0ff">Blue</option>
<option value="#ffffcc">Yellow</option>
</select>
</div>
<div>
<label>우선순위 : </label>
<select
value={priority}
id="priority"
onChange={(e) => setPriority(e.target.value)}
>
<option value="low">Low</option>
<option value="high">High</option>
</select>
</div>
</OptionsBox>
</Box>
</FixedContainer>
);
};
export default CreateNoteModal;
기존 default로 생성했던 태그들 또는 새로운 태그를 추가하거나 뺄 수 있다.
components > Modal > TagsModal.tsx
...
interface TagsModalProps {
type: string,
addedTags?: Tag[], // optional
handleTags?: (tag: string, type: string) => void
}
const TagsModal = ({ type, addedTags, handleTags }: TagsModalProps) => { // add 또는 edit
const dispatch = useAppDispatch();
const { tagsList } = useAppSelector((state) => state.tags); // 처음에 만들어놓은 tag들
const [inputText, setInputText] = useState('');
const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if(!inputText) {return;}
dispatch(addTags({tag: inputText.toLowerCase(), id: v4() }));
setInputText('');
}
const deleteTagsHandler = (tag: string, id: string) => {
dispatch(deleteTags(id));
dispatch(removeTags({ tag }));
}
return (
<FixedContainer>
<Box>
<div className="editTags__header">
<div className="editTags__title">
{type === "add" ? "ADD" : "Edit"} Tags
</div>
<DeleteBox
className="editTags__close"
onClick={() => dispatch(toggleTagsModal({ type, view: false }))}
>
<FaTimes />
</DeleteBox>
</div>
<form onSubmit={submitHandler}>
<StyledInput
type="text"
value={inputText}
placeholder="new tag..."
onChange={(e) => setInputText(e.target.value)}
/>
</form>
<TagsBox>
{tagsList.map(({ tag, id }) => (
<li key={id}>
<div className="editTags__tag">{getStandardName(tag)}</div>
{type === "edit" ? (
<DeleteBox onClick={() => deleteTagsHandler(tag, id)}>
<FaTimes />
</DeleteBox>
) : (
<DeleteBox>
{addedTags?.find(
(addedTag: Tag) => addedTag.tag === tag.toLowerCase()
) ? (
<FaMinus onClick={() => handleTags!(tag, "remove")} />
// type단언 해주기. edit아니고 add일때 분명히 undefined가 아닐것이기때문에
// null이나 undefined가 아니다라는 뜻
): (
<FaPlus onClick={() => handleTags!(tag, "add")}/>
)}
</DeleteBox>
)}
</li>
))}
</TagsBox>
</Box>
</FixedContainer>
);
}
export default TagsModal
CreateNoteModal.tsx에서 제목 하단에 <TextEditor color={noteColor} value={value} setValue={setValue} />
import, 추가하고
src > components > TextEditor.tsx
부분 작성
import React from "react";
import { Container } from './TextEditor.styles';
import ReactQuill from 'react-quill';
import 'react-quill/dist/quill.snow.css';
interface TextEditorProps {
value: string,
setValue: React.Dispatch<React.SetStateAction<string>>,
color: string
}
const formats = [
"bold",
"italic",
"underline",
"strike",
"list",
"color",
"background",
"image",
"blockquote",
"code-block",
];
const modules = {
toolbar: [
[{ list: "ordered" }, { list: "bullet" }],
[],
["italic", "underline", "strike"],
[],
[{ color: [] }, { background: [] }],
[],
["image", "blockquote", "code-block"],
],
};
const TextEditor = ({ color, value, setValue }: TextEditorProps) => {
return (
<Container noteColor={color}>
<ReactQuill
formats={formats}
modules={modules}
theme="snow"
value={value}
onChange={setValue}
/>
</Container>
);
};
export default TextEditor
CreateNoteModal.tsx
에서 저장하기/생성하기 버튼에 이벤트핸들러 넣고
<ButtonFill onClick={createNoteHanlder}>
{editNote ? (
<span>저장하기</span>
) : (
<>
<FaPlus />
<span>생성하기</span>
</>
)}
</ButtonFill>
그 핸들러 기능을 모달 return부분 상단에 작성해준다.
1. 먼저 유효성검사를 하고
2. Note의 Partial로(부분만 사용하는 유틸리티타입) 사용하는 note 데이터 생성
3. 수정일때는 원래 가지고있던 editNote데이터들과 새로 만들어주는 note를 넣고 setMainNotes에서 id가 payload.id와 같은 노트를 변경해주고 원래있는건 그대로 넣어줌
4. 새로 생성할때는 note객체를 만들어서 push한다.
const createNoteHanlder = () => {
// 유효성검사
if(!noteTitle) {
toast.error('타이틀을 작성해주세요.');
return;
} else if(value === "<p><br/></p>") {
toast.error('내용을 작성해주세요.');
}
const date = dayjs().format("DD/MM/YYYY h:mm A");
let note: Partial<Note> = {
title: noteTitle,
content: value,
tags: addedTags,
color: noteColor,
priority,
editedTime: new Date().getTime()
}
if(editNote) { // 수정
note = { ...editNote, ...note }
} else {
note = {
...note,
date,
createdTime: new Date().getTime(),
editedTime: null,
isPinned: false,
isRead: false,
id: v4()
}
}
dispatch(setMainNotes(note));
dispatch(toggleCreateNoteModal(false));
dispatch(setEditNote(null));
};