diff --git a/src/pages/achivement/AchivementPage.tsx b/src/pages/achivement/AchivementPage.tsx index edc735e..3443ccb 100644 --- a/src/pages/achivement/AchivementPage.tsx +++ b/src/pages/achivement/AchivementPage.tsx @@ -26,7 +26,7 @@ export interface AchivementPageProps { } export const AchivementPage = ({ month }: AchivementPageProps) => { // FIXME: routine -> note로 - const [type, setType] = useState("routine"); + const [type, setType] = useState("note"); const { view, leafBgColor } = useLeaf(); return ( @@ -66,9 +66,9 @@ export const AchivementPage = ({ month }: AchivementPageProps) => { }}>
{type === "note" ? - + : - + }
diff --git a/src/shared/components/swipeable-calender/SwipeableCalendar.tsx b/src/shared/components/swipeable-calender/SwipeableCalendar.tsx index 2454451..ced8698 100644 --- a/src/shared/components/swipeable-calender/SwipeableCalendar.tsx +++ b/src/shared/components/swipeable-calender/SwipeableCalendar.tsx @@ -31,25 +31,31 @@ interface Props { children: (month: Month, index?: number) => React.ReactNode; verticalHeight?: number; maxWidth?: number + onMonthChange?: (month: Month) => void; } export const SwipeableCalendar = ({ month: propsMonth, children, verticalHeight, maxWidth, + onMonthChange }: Props) => { const [months, setMonths] = useState(loadMonths(propsMonth)); const [activeMonth, setActiveMonth] = useState(propsMonth); - const resetMonths = useCallback((month: Month) => { - setActiveMonth(month); - setMonths(loadMonths(month)); - }, []); - + const onSlideChange = useCallback((month: Month) => { setActiveMonth(month); - }, []); + onMonthChange?.(month); + }, [onMonthChange]); + + const resetMonths = useCallback((month: Month) => { + setMonths(loadMonths(month)); + onSlideChange(month); + }, [onSlideChange]); + + return (
(tagName: keyof HTMLElementTagNameMap) => { + const parentRef = useRef

(null); + const childRef = useRef(null); + + // 최초 1회 초기화 실행 + useEffect(() => { + create(); + return destroy; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const destroy = useCallback(() => { + if(!parentRef.current || !childRef.current) return; + parentRef.current.removeChild(childRef.current); + childRef.current = null; + }, []); + + /** + * 이미 있으면 안 만든다. + */ + const create = useCallback(() => { + if(!parentRef.current || childRef.current) return; + const child = document.createElement(tagName); + parentRef.current.appendChild(child); + childRef.current = child as C; + }, [tagName]); + + + return { + parentRef, + childRef, + create, + destroy + } +} \ No newline at end of file diff --git a/src/shared/period/month.ts b/src/shared/period/month.ts index e305ead..35b36b6 100644 --- a/src/shared/period/month.ts +++ b/src/shared/period/month.ts @@ -2,6 +2,8 @@ import { Day } from "./day"; +export type MonthFormat = string; + export class Month { readonly #startDay: Day; readonly #endDay: Day; @@ -45,6 +47,10 @@ export class Month { return new Month( this.#startDay.clone(m => m.subtract(amount, "month")) ); + } + + format(): MonthFormat { + return this.#startDay.format("YYYY-MM"); } } \ No newline at end of file diff --git a/src/shared/zustand/create-store-context.tsx b/src/shared/zustand/create-store-context.tsx index ed2ce51..6be64ac 100644 --- a/src/shared/zustand/create-store-context.tsx +++ b/src/shared/zustand/create-store-context.tsx @@ -30,7 +30,7 @@ type ExtractState = S extends { getState: () => infer T; } ? T : never; type ReadonlyStoreApi = Pick, 'getState' | 'getInitialState' | 'subscribe'>; -export type UseBoundStore> = { +type UseBoundStore> = { (): ExtractState; (selector: (state: ExtractState) => U): U; } & S; diff --git a/src/widgets/routine-achivement/model/resolve-routine-tiles.ts b/src/widgets/routine-achivement/model/resolve-routine-tiles.ts deleted file mode 100644 index 372a1cd..0000000 --- a/src/widgets/routine-achivement/model/resolve-routine-tiles.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NoteEntity, noteRepository, RoutineNote } from "@entities/note"; -import { Tile, TileMap } from "./types"; -import { Month } from "@shared/period/month"; -import { DayFormat } from "@shared/period/day"; - - - -/** - * - * @param routineName - * @param month - */ -export const resolveRoutineTiles = async (routineName: string, month: Month): Promise => { - const notes = await noteRepository.loadBetween(month.startDay, month.endDay); - const tileMap = new Map(); - - const end = month.startDay.daysInMonth(); - for(let d = 1; d <= end; d++) { - const day = month.startDay.clone(m => m.add(d-1, "day")); - const note = notes.find(note => note.day.isSameDay(day)); - - let tile: Tile | null = null; - // 노트가 존재 - if(note){ - const routineTask = NoteEntity.findTask(note, routineName); - // 노트에 routine이 존재 - if(routineTask){ - tile = { - day, - state: routineTask.state - } - } - } - if(!tile){ - tile = { - day, - state: "inactive" - } - } - tileMap.set(day.format(), tile); - } - return tileMap; -} \ No newline at end of file diff --git a/src/widgets/routine-achivement/model/use-routine-selector.ts b/src/widgets/routine-achivement/model/use-routine-selector.ts new file mode 100644 index 0000000..97d677c --- /dev/null +++ b/src/widgets/routine-achivement/model/use-routine-selector.ts @@ -0,0 +1,23 @@ +import { MonthFormat } from "@shared/period/month"; +import { create } from "zustand"; + + +type UseRoutineSelector = { + currentRoutine: string; + setCurrentRoutine: (routine: string) => void; + routineOptionsPerMonth: ReadonlyMap; + addRoutineOptionsPerMonth: (monthFormat: MonthFormat, routineOptions: string[]) => void; +} + +export const useRoutineSelector = create((set, get) => ({ + currentRoutine: "", + setCurrentRoutine: (routine: string) => set({ currentRoutine: routine }), + routineOptionsPerMonth: new Map(), + addRoutineOptionsPerMonth: (monthFormat: MonthFormat, routineOptions: string[]) => { + const map = get().routineOptionsPerMonth as Map; + if(!map.has(monthFormat)){ + map.set(monthFormat, routineOptions); + set({ routineOptionsPerMonth: new Map(map) }); + } + } +})) \ No newline at end of file diff --git a/src/widgets/routine-achivement/ui/CalendarSlide.tsx b/src/widgets/routine-achivement/ui/CalendarSlide.tsx index 1b9b8f3..6d088f6 100644 --- a/src/widgets/routine-achivement/ui/CalendarSlide.tsx +++ b/src/widgets/routine-achivement/ui/CalendarSlide.tsx @@ -1,57 +1,118 @@ /** @jsxImportSource @emotion/react */ -import { noteRepository } from "@entities/note"; +import { isRoutineTask, NoteEntity, noteRepository } from "@entities/note"; import { BaseCalendar } from "@shared/components/BaseCalendar"; -import { Day } from "@shared/period/day"; +import { Day, DayFormat } from "@shared/period/day"; import { Month } from "@shared/period/month"; import { useAsync } from "@shared/utils/use-async"; -import { useCallback } from "react"; -import { CalendarTile } from "./CalendarTile"; +import { useCallback, useEffect, useMemo } from "react"; import { Tile } from "../model/types"; -import { resolveRoutineTiles } from "../model/resolve-routine-tiles"; +import { useRoutineSelector } from "../model/use-routine-selector"; +import { CalendarTile } from "./CalendarTile"; interface Props { month: Month; - routineName: string; } -export const CalendarSlide = ({ routineName, month }: Props) => { - const tilesAsync = useAsync(async () => await resolveRoutineTiles(routineName, month), [month]); +export const CalendarSlide = ({ + month, +}: Props) => { + const notesAsync = useAsync(() => noteRepository.loadBetween(month.startDay, month.endDay), [month]); + const { currentRoutine, addRoutineOptionsPerMonth, routineOptionsPerMonth } = useRoutineSelector(); + + + const tileMap = useMemo | null>(() => { + if(notesAsync.loading || !notesAsync.value) return null; + + const notes = notesAsync.value; + const map = new Map(); + + const end = month.startDay.daysInMonth(); + for(let d = 1; d <= end; d++) { + const day = month.startDay.clone(m => m.add(d-1, "day")); + const note = notes.find(note => note.day.isSameDay(day)); + + let tile: Tile | null = null; + // 노트가 존재 + if(note){ + const routineTask = NoteEntity.findTask(note, currentRoutine); + // 노트에 routine이 존재 + if(routineTask){ + tile = { + day, + state: routineTask.state + } + } + } + if(!tile){ + tile = { + day, + state: "inactive" + } + } + map.set(day.format(), tile); + } + return map; + }, [currentRoutine, month, notesAsync.loading, notesAsync.value]); + + + // routineOptions를 최초 계산하여 routineOptionsPerMonth에 추가한다. + useEffect(() => { + if(notesAsync.loading || !notesAsync.value) return; + if(routineOptionsPerMonth.has(month.format())) return; + + const notes = notesAsync.value; + const existingRoutineNames = notes.flatMap(note => NoteEntity + .flatten(note) + .filter(isRoutineTask) + .map(routine => routine.name) + ); + const routineOptions = Array.from(new Set(existingRoutineNames)); + addRoutineOptionsPerMonth(month.format(), routineOptions); + }, [addRoutineOptionsPerMonth, month, notesAsync.loading, notesAsync.value, routineOptionsPerMonth]); + const tile = useCallback((day: Day) => { if(day.month !== month.monthNum) return <>; - const tile = tilesAsync.value?.get(day.format()); + if(!tileMap) return <>; + + const tile = tileMap.get(day.format()); if(!tile) throw new Error(`tile not found for ${day.format()}`); return ; - }, [month.monthNum, tilesAsync.value]); + }, [month.monthNum, tileMap]); + const onTileClick = useCallback((day: Day) => { console.log(day); }, []); + const tileDisabled = useCallback((day: Day) => { return day.month !== month.monthNum; }, [month]); - - if(tilesAsync.loading) return

Loading...
; + + if(!tileMap) return <>; + return ( - {}} - onTileClick={onTileClick} - tile={tile} - showNavigation={false} - tileDisabled={tileDisabled} - styleOptions={{ - tileContainer: { - gap: "0", - }, - tile: { - border: "0", - overflow: "visible !important", - } - }} - /> + <> + {}} + onTileClick={onTileClick} + tile={tile} + showNavigation={false} + tileDisabled={tileDisabled} + styleOptions={{ + tileContainer: { + gap: "0", + }, + tile: { + border: "0", + overflow: "visible !important", + } + }} + /> + ); }; diff --git a/src/widgets/routine-achivement/ui/CalendarTile.tsx b/src/widgets/routine-achivement/ui/CalendarTile.tsx index 90a01b1..0025576 100644 --- a/src/widgets/routine-achivement/ui/CalendarTile.tsx +++ b/src/widgets/routine-achivement/ui/CalendarTile.tsx @@ -2,16 +2,11 @@ import { TaskCheckbox } from "@features/task-el"; import { Tile } from "../model/types" import { Circle } from "@shared/components/Circle"; -import { getCustomAccentHSL } from "@shared/components/obsidian-accent-color"; import { Day } from "@shared/period/day"; import { Badge } from "@mui/material"; - - - - type Props = { tile: Tile; } diff --git a/src/widgets/routine-achivement/ui/RoutineAchivementWidget.tsx b/src/widgets/routine-achivement/ui/RoutineAchivementWidget.tsx index 695f80d..58d1d20 100644 --- a/src/widgets/routine-achivement/ui/RoutineAchivementWidget.tsx +++ b/src/widgets/routine-achivement/ui/RoutineAchivementWidget.tsx @@ -1,43 +1,35 @@ /** @jsxImportSource @emotion/react */ -import { Month } from '@shared/period/month'; import { SwipeableCalendar } from '@shared/components/swipeable-calender/SwipeableCalendar'; +import { Month } from '@shared/period/month'; +import { useState } from 'react'; import { CalendarSlide } from './CalendarSlide'; -import { TEXT_CSS } from '@shared/components/text-style'; -import { useLeaf } from '@shared/view/use-leaf'; +import { RoutineSelector } from './RoutineSelector'; export interface Props { month: Month; - routineName: string; height: number; maxWidth: number; } export const RoutineAchivementWidget = ({ - month, + month: propsMonth, height, - routineName, maxWidth }: Props) => { - const { view, leafBgColor } = useLeaf(); + const [month, setMonth] = useState(propsMonth); return ( -
-
- {routineName} -
+ <> + - {month => } + {month => } -
- 세차니 -
-
+ ) } \ No newline at end of file diff --git a/src/widgets/routine-achivement/ui/RoutineSelector.tsx b/src/widgets/routine-achivement/ui/RoutineSelector.tsx new file mode 100644 index 0000000..e01ad10 --- /dev/null +++ b/src/widgets/routine-achivement/ui/RoutineSelector.tsx @@ -0,0 +1,96 @@ +/** @jsxImportSource @emotion/react */ + +import { Button } from '@shared/components/Button'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import { useRoutineSelector } from "../model/use-routine-selector"; +import { useCallback, useMemo, useState } from "react"; +import { useLeaf } from '@shared/view/use-leaf'; +import { maxWidth } from '@mui/system'; +import { Month } from '@shared/period/month'; + + + + + + + + + +type Props = { + month: Month; + className?: string; + maxWidth?: number; +} +export const RoutineSelector = ({ + month, + maxWidth, + className +}: Props) => { + const { currentRoutine, setCurrentRoutine, routineOptionsPerMonth } = useRoutineSelector(); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const { view } = useLeaf(); + + + const handleRoutineOptionClick = useCallback((routine: string) => { + setCurrentRoutine(routine); + // menu close + setAnchorEl(null); + }, [setCurrentRoutine]); + + + return ( +
+ + setAnchorEl(null)} + css={{ + ".MuiPaper-root":{ + width: view.contentEl.clientWidth, + transform: "translateX(16px) !important", + maxHeight: "500px", + }, + "ul": { + "li":{ + minHeight: "20px !important", + borderTop: "1px solid var(--color-base-30)", + }, + }, + "li:nth-child(1)": { + borderTop: "none" + } + }} + > + {(() => { + const options = routineOptionsPerMonth.get(month.format()); + if(!options || options.length === 0) return no routines...; + + return options.map(routine => ( + handleRoutineOptionClick(routine)} + > + {routine} + + )) + })()} + +
+ ) +} \ No newline at end of file