Navbar.tsx
const Navbar = () => {
const dispatch = useAppDispatch();
const { pathname, state } = useLocation();
if (pathname === "/404") {
return null;
} // 404일때 안나오게
return (
<StyledNav>
<div className="nav__menu">
<FiMenu onClick={() => dispatch(toggleMenu(true))} />
</div>
<Container>
<div className="nav__page-title">{getStandardName(state)}</div>
{state !== "Trash" && state !== "Archive" && (
<ButtonFill
onClick={() => dispatch(toggleCreateNoteModal(true))}
className="nav__btn"
>
<span>+</span>
</ButtonFill>
)}
</Container>
</StyledNav>
);
}
export default Navbar
menuSlice에서 toggleMenu
modalSlice에서 toggleCreateNoteModal
menuSlice.ts
const menuSlice = createSlice({
name: "menu",
initialState,
reducers: {
toggleMenu: (state, action) => {
state.isOpen = action.payload
}
}
})
export const { toggleMenu } = menuSlice.actions;
modalSlice.ts
const modalSlice = createSlice({
name: "modal",
initialState,
reducers: {
toggleTagsModal: (state, { payload }) => {
const { type, view } = payload;
if(type === "add") {
state.viewAddTagsModal = view;
} else {
state.viewEditTagsModal = view;
}
},
toggleCreateNoteModal: (state, action) => {
state.viewCreateNoteModal = action.payload;
},
toggleFiltersModal: (state, action) => {
state.viewFiltersModal = action.payload;
},
},
});
export const { toggleTagsModal, toggleCreateNoteModal, toggleFiltersModal } = modalSlice.actions;
actions모아서 내보내도되고... 그냥 가져와도되는데 일단 그냥 쓰도록 한다.
utils/getStandardName.tsx
const getStandardName = (name: string) => {
return (
name?.slice(0, 1).toUpperCase() + name?.slice(1, name.length).toLocaleLowerCase()
)
}
export default getStandardName;
Navbar에서 {getStandardName(state)}
로 페이지이름 출력
Sidebar.tsx
const items = [
{ icon: <FaArchive />, title: "Archive", id: v4() },
{ icon: <FaTrash />, title: "Trash", id: v4() },
];
const Sidebar = () => {
const dispatch = useAppDispatch();
const { isOpen } = useAppSelector((state) => state.menu);
const { tagsList } = useAppSelector((state) => state.tags);
const { pathname } = useLocation();
if(pathname === "/404") { return null;}
return (
// prop 이름을 openMenu라고 쓰면 다음과 같은 에러가 콘솔에 떠서 $를 붙여서 $openMenu라고 쓴다.
// If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase openMenu instead.
<Container $openMenu={isOpen ? "open" : ""}>
<MainBox $openMenu={isOpen ? "open" : ""}>
<StyledLogo>
<h1>Keep</h1>
</StyledLogo>
<ItemsBox>
{/* note item */}
<li onClick={() => dispatch(toggleMenu(false))}>
<NavLink
to={"/"}
state={`notes`}
className={({ isActive }) =>
isActive ? "active-item" : "inactive-item"
}
>
<span>
<FaLightbulb />
</span>
<span>Notes</span>
</NavLink>
</li>
{tagsList?.map(({ tag, id }) => (
<li key={id} onClick={() => dispatch(toggleMenu(false))}>
<NavLink
to={`/tag/${tag}`}
state={`${tag}`}
className={({ isActive }) =>
isActive ? "active-item" : "inactive-item"
}
>
<span>
<FaTag />
</span>
<span>{getStandardName(tag)}</span>
</NavLink>
</li>
))}
{/* edit tag item */}
<li
className="sidebar__edit-item"
onClick={() =>
dispatch(toggleTagsModal({ type: "edit", view: true }))
}
>
<NavLink
to={"/"}
state={`notes`}
className={({ isActive }) =>
isActive ? "active-item" : "inactive-item"
}
>
<span>
<MdEdit />
</span>
<span>Edit Notes</span>
</NavLink>
</li>
{/* archive */}
{items.map(({ icon, title, id }) => (
<li key={id} onClick={() => dispatch(toggleMenu(false))}>
<NavLink
to={`/${title.toLocaleLowerCase()}`}
state={`${title}`}
className={({ isActive }) =>
isActive ? "active-item" : "inactive-item"
}
>
<span>{icon}</span>
<span>{getStandardName(title)}</span>
</NavLink>
</li>
))}
</ItemsBox>
</MainBox>
</Container>
);
}
export default Sidebar
tagsSlice.ts
const tagsSlice = createSlice({
name: 'tags',
initialState,
reducers: {
addTags: (state, { payload }) => {
if(state.tagsList.find(({tag}) => tag === payload.tag)) { // 있는 태그는 추가하지않고 이미있다는 알림
toast.warning("이미 존재하는 태그입니다. ");
} else {
state.tagsList.push(payload); // 내부에서 immer라는 모듈로 처리되기때문에 불변성안지키면서 push 할수있음
toast.info("새로운 태그가 등록되었습니다.");
}
},
deleteTags: (state, { payload }) => {
state.tagsList = state.tagsList.filter(({id}) => id !== payload); //같지 않은것만 넣어줌. 즉, 같은건 지워짐
toast.info("태그가 삭제되었습니다.");
}
},
})
export const { addTags, deleteTags } = tagsSlice.actions;
할당된 태그를 노트에서 지운다.
notesListSlice.ts
const notesListSlice = createSlice({
name: 'notesList',
initialState,
reducers: {
removeTags: (state, { payload }) => {
state.mainNotes = state.mainNotes.map((note) => ({
...note, // spread operate
tags: note.tags.filter(({ tag }) => tag !== payload.tag), // payload에 같은 태그는 삭제하고 넣어준다
}));
}
}
})
export const { removeTags } = notesListSlice.actions;
TagsModal.tsx
interface TagsModalProps {
type: string
}
const TagsModal = ({ type }: 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)}
<DeleteBox onClick={() => deleteTagsHandler(tag, id)}>
<FaTimes />
</DeleteBox>
</div>
</li>
))}
</TagsBox>
</Box>
</FixedContainer>
)
}
export default TagsModal
map, filter, reduce 메소드는 불변성을 지켜준다.
원본 자체를 바꾸는게 아니라 새롭게 배열을 만들어서 반환하기때문에...
App.tsx
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
function App() {
const { viewEditTagsModal } = useAppSelector((state) => state.modal);
return (
<div className="app">
{viewEditTagsModal && <TagsModal type='edit' />}
<ToastContainer
position='bottom-right'
theme='light'
pauseOnHover
autoClose={1500}
/>
viewEditTagsModal가 true일때 편집하는 <TagsModal>
을 띄워준다.
아래에 <ToastContainer>
컴포넌트도 넣어줌