debug: 캘린더 nav로 month 변경할 수 없는 문제를 해결

This commit is contained in:
sechan100 2025-01-13 17:45:35 +09:00
parent d4600c66d8
commit 0e2df22983
40 changed files with 691 additions and 290 deletions

View file

@ -1,3 +1,3 @@
node_modules/
**/dist/**
main.js

View file

@ -17,6 +17,7 @@
"sourceType": "module"
},
"rules": {
"no-warning-comments": ["warn", { "terms": ["TODO", "FIXME"], "location": "start" }],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["off", {}],
"@typescript-eslint/ban-ts-comment": "off",

View file

@ -63,7 +63,7 @@ const context = await esbuild.context({
},
format: "cjs", // CommonJS 포맷
target: "es2018",
logLevel: "info",
logLevel: "warning",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js", // JavaScript 번들 파일

View file

@ -23,12 +23,12 @@ const tabCss = css({
boxShadow: "none !important",
backgroundColor: "transparent !important",
minHeight: "0 !important",
height: tabsHeight,
fontSize: "0.7em",
})
export const DailyRoutineView = () => {
const { tab, route } = useTabRoute();
const { view, leafBgColor } = useLeaf();
useEffect(() => {
@ -39,7 +39,7 @@ export const DailyRoutineView = () => {
const onTabChange = useCallback((e: React.SyntheticEvent, tab: DrTabType) => {
switch(tab){
case "note":
route(tab, { day: Day.now() });
route(tab, { day: Day.today() });
break;
case "achivement":
route(tab, { month: Month.now() });
@ -74,7 +74,7 @@ export const DailyRoutineView = () => {
zIndex: 1000,
backgroundColor: leafBgColor,
position: "fixed",
height: tabsHeight,
minHeight: 0,
width: "100%",
left: "0",
bottom: tabsBottomGap,

View file

@ -20,7 +20,7 @@ export const RouitneNoteTab = () => {
}
return params;
} else {
return { day: Day.now() };
return { day: Day.today() };
}
}, [routeParams, tab]);

View file

@ -35,7 +35,7 @@ export const useRoutineMutationMerge: UseRoutineMutationMerge = () => {
setNote(manuallyMutatedNote);
await noteRepository.updateIfExist(manuallyMutatedNote);
// merge
let notes = await noteRepository.loadBetween(Day.now(), Day.max());
let notes = await noteRepository.loadBetween(Day.today(), Day.max());
notes = notes.filter(n => !n.day.isSameDay(currentNote.day));
const noteCreator = await RoutineNoteCreator.withLoadFromRepositoryAsync();
for(const note of notes){
@ -44,7 +44,7 @@ export const useRoutineMutationMerge: UseRoutineMutationMerge = () => {
await noteRepository.persist(merged);
}
} else {
const notes = await noteRepository.loadBetween(Day.now(), Day.max());
const notes = await noteRepository.loadBetween(Day.today(), Day.max());
const noteCreator = await RoutineNoteCreator.withLoadFromRepositoryAsync();
for(const note of notes){
const merged = mergeNote(note, noteCreator);

View file

@ -1,6 +1,6 @@
import { NotePerformance } from "@entities/note";
import { Circle } from "@shared/components/Circle";
import clsx from "clsx";
import { getCustomAccentHSL } from "@shared/components/obsidian-accent-color";
import React from "react";
const cleanPercentage = (percentage: number) => {
@ -10,8 +10,13 @@ const cleanPercentage = (percentage: number) => {
};
const completionCircleColor = "hsla(var(--color-accent-2-hsl), 1)";
const accomplishmentCircleColor = "hsla(var(--color-accent-hsl), 1)";
const completionCircleColor = getCustomAccentHSL({
h: 1,
s: 1,
l: 1.2,
a: 1,
})
const accomplishmentCircleColor = "hsl(var(--color-accent-hsl))";
interface PercentageCircleProps {
@ -31,11 +36,11 @@ export const PerformanceCircle = React.memo(({
const accomplishment = cleanPercentage(performance.accomplishment);
const completion = cleanPercentage(performance.completion);
return (
<svg width={width??"100%"} data-percentage={completion} viewBox="0 0 200 200" className={className}>
<svg width={width??"100%"} viewBox="0 0 200 200" className={className}>
<g transform="rotate(-90 100 100)" >
<Circle transition={false} color="#ececec" percentage={100} />
<Circle transition={transition} color={completionCircleColor} percentage={completion} />
<Circle transition={transition} color={accomplishmentCircleColor} percentage={accomplishment} />
<Circle transition={transition} color={completion ? completionCircleColor : "transparent"} percentage={completion} />
<Circle transition={transition} color={completion ? accomplishmentCircleColor : "transparent"} percentage={accomplishment} />
</g>
<text
x="50%"

View file

@ -130,8 +130,8 @@ export const useGroupDnd = ({
const div = document.createElement("div");
div.setCssStyles({
backgroundColor: "red",
width: "0.01px",
height: "0.01px",
width: "0.1px",
height: "0.1px",
})
div.id = "dr-pseudo-preview";
view.containerEl.appendChild(div);

View file

@ -149,8 +149,8 @@ export const useTaskDnd = ({
const div = document.createElement("div");
div.setCssStyles({
backgroundColor: "red",
width: "0.01px",
height: "0.01px",
width: "0.1px",
height: "0.1px",
})
div.id = "dr-pseudo-preview";
view.containerEl.appendChild(div);

View file

@ -2,3 +2,4 @@ export { TaskDndContext } from "./dnd/dnd-context";
export { BaseTaskFeature } from "./ui/BaseTaskFeature";
export { BaseTaskGroupFeature } from "./ui/BaseTaskGroupFeature";
export { renderTask } from "./ui/render-task-widget";
export { TaskCheckbox } from "./ui/TaskCheckbox";

View file

@ -7,5 +7,5 @@ export const changeTaskState = async (note: RoutineNote, taskName: string, state
task.state = state;
await noteRepository.saveOnUserConfirm(note);
return { ...note };
return NoteEntity.replaceTask(note, taskName, task);
}

View file

@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTaskDnd } from '../dnd/use-task-dnd';
import { changeTaskState } from '../model/change-task-state';
import { CancelLineName } from './CancelLineName';
import { Checkbox } from './Checkbox';
import { TaskCheckbox } from './TaskCheckbox';
import { OptionIcon } from './OptionIcon';
import { baseHeaderStyle, dragReadyStyle, draggingStyle, elementHeight, pressedStyle } from './base-element-style';
import { DELAY_TOUCH_START } from '../dnd/dnd-context';
@ -148,8 +148,14 @@ export const BaseTaskFeature = React.memo(<T extends Task>({
lineHeight: 1
}}
>
<Checkbox state={task.state} />
<CancelLineName name={task.name} cancel={task.state !== "un-checked"} transparentLine={task.state === "failed"} />
<TaskCheckbox
state={task.state}
size={13}
css={{
marginRight: "0.5em",
}}
/>
<CancelLineName name={task.name} cancel={task.state !== "un-checked"} />
</Touchable>
<OptionIcon onOptionMenu={(m) => onOptionMenu(m, task)} />
{indicator}

View file

@ -5,7 +5,7 @@ import { Icon } from '@shared/components/Icon';
import { Touchable } from '@shared/components/Touchable';
import { dr } from '@shared/utils/daily-routine-bem';
import { useLeaf } from '@shared/view/use-leaf';
import React, { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useGroupDnd } from '../dnd/use-group-dnd';
import { baseHeaderStyle, draggingStyle, dragReadyStyle, elementHeight, pressedStyle } from './base-element-style';
import { DELAY_TOUCH_START } from '../dnd/dnd-context';
@ -38,7 +38,14 @@ export const BaseTaskGroupFeature = React.memo(({
const groupRef = useRef<HTMLDivElement>(null);
const [groupMode, setGroupMode] = useState<GroupMode>("idle");
const bgColor = useLeaf(s=>s.leafBgColor);
const [open, setOpen] = useState(true);
const isAllSubTasksChecked = group.children.every(t => t.state !== "un-checked")
const [open, setOpen] = useState(isAllSubTasksChecked ? false : true);
useEffect(() => {
if(isAllSubTasksChecked){
setOpen(false);
}
}, [isAllSubTasksChecked])
const onElDrop = useCallback((newNote: RoutineNote, dropped: TaskGroup) => {
onGroupReorder?.(newNote, dropped);
@ -158,8 +165,7 @@ export const BaseTaskGroupFeature = React.memo(({
>
<CancelLineName
name={group.name}
cancel={group.children.every(t => t.state !== "un-checked")}
transparentLine={group.children.length === 0}
cancel={isAllSubTasksChecked}
/>
</AccordionSummary>
</Touchable>

View file

@ -36,17 +36,15 @@ const cancelLineStyle = css({
interface Props {
name: string;
cancel: boolean;
transparentLine?: boolean;
}
export const CancelLineName = ({
name,
cancel,
transparentLine
}: Props) => {
return (
<span
className={bem("name")}
css={[baseStyle, cancel && cancelTextStyle, cancel && !transparentLine && cancelLineStyle]}
css={[baseStyle, cancel && cancelTextStyle && cancelLineStyle]}
>
{name}
</span>

View file

@ -1,140 +0,0 @@
/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";
import { TaskState } from "@entities/note";
import { Icon } from "@shared/components/Icon";
import { dr } from "@shared/utils/daily-routine-bem";
import { useMemo } from "react";
import { CSSTransition, SwitchTransition } from "react-transition-group";
const bem = dr("task-cbx")
const CheckMarkWrapper = ({ children }: { children: React.ReactNode }) => {
return (
<svg
viewBox="0 0 14 12"
css={{
position: "relative",
width: "calc(7/8 * 1em)",
height: "calc(3/4 * 1em)",
top: "calc(-1/16 * 1em)",
fill: "none",
strokeLinecap: "round",
strokeLinejoin: "round",
}}
>
{children}
</svg>
)
}
const AccomplishCheckMark = () => {
return (
<CheckMarkWrapper>
<polyline
points="1 7.6 5 11 13 1"
css={{
strokeWidth: 2,
stroke: "var(--color-accent-1)"
}}
/>
</CheckMarkWrapper>
)
}
const FailedCheckMark = () => {
return (
<CheckMarkWrapper>
<polyline
points="1 1 10 10"
css={{
strokeWidth: 2,
stroke: "var(--color-accent-1)"
}}
/>
<polyline
points="10 1 1 10"
css={{
strokeWidth: 2,
stroke: "var(--color-accent-1)"
}}
/>
</CheckMarkWrapper>
)
}
interface Props {
state: TaskState;
}
export const Checkbox = ({ state }: Props) => {
const isChecked = useMemo(() => state !== "un-checked", [state]);
return (
<span
className={bem("cbx")}
css={{
position: "relative",
top: "calc(1/32 * 1em)",
display: "inline-block",
width: "calc(14/16 * 1em)",
height: "calc(14/16 * 1em)",
marginRight: "calc(0.5em)",
border: "calc(1/16 * 1em) solid #c8ccd4",
borderColor: isChecked ? "transparent" : undefined,
borderRadius: "3px",
cursor: "pointer",
transition: "scale",
// 체크될 때 이펙트
"&::after": {
content: "''",
position: "absolute",
top: "calc(5/16 * 1em)",
left: "calc(5/16 * 1em)",
width: "calc(1/8 * 1em)",
height: "calc(1/8 * 1em)",
borderRadius: "2px",
boxShadow: "0 calc(9/8 * -1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * 1em) calc(3/4 * -1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(9/8 * 1em) 0 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * 1em) calc(3/4 * 1em) 0 hsla(var(--color-accent-1-hsl), 1.0), 0 calc(9/8 * 1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * -1em) calc(3/4 * 1em) 0 hsla(var(--color-accent-1-hsl), 1.0), calc(9/8 * -1em) 0 0 hsla(var(--color-accent-1-hsl), 1.0), calc(3/4 * -1em) calc(3/4 * -1em) 0 hsla(var(--color-accent-1-hsl), 1.0)",
transform: "scale(0)",
...(isChecked && {
transform: "scale(1)",
opacity: "0",
transition: "all 0.6s ease",
})
},
// transition
"& .fade-enter": {
transform: "scale(0)",
opacity: 0,
},
"& .fade-enter-active": {
transform: "scale(1)",
opacity: 1,
transition: "all 0.5s ease",
},
}}
>
<CSSTransition
in={state === "accomplished"}
key="accomplish"
timeout={500}
exit={false}
classNames="fade"
unmountOnExit
children={<AccomplishCheckMark />}
/>
<CSSTransition
in={state === "failed"}
key="failed"
timeout={500}
exit={false}
classNames="fade"
unmountOnExit
children={<FailedCheckMark />}
/>
</span>
)
}

View file

@ -0,0 +1,170 @@
/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";
import { TaskState } from "@entities/note";
import { Icon } from "@shared/components/Icon";
import { getCustomAccentHSL } from "@shared/components/obsidian-accent-color";
import { dr } from "@shared/utils/daily-routine-bem";
import { useMemo } from "react";
import { CSSTransition, SwitchTransition } from "react-transition-group";
const bem = dr("task-cbx")
const CheckMarkWrapper = ({ children, size }: { children: React.ReactNode, size: number }) => {
return (
<svg
viewBox="0 0 100 100"
width={size}
height={size}
css={{
position: "relative",
fill: "none",
strokeLinecap: "round",
strokeLinejoin: "round",
}}
>
{children}
</svg>
)
}
type AccomplishCheckMarkProps = {
size: number;
custom?: React.ReactNode;
}
const AccomplishCheckMark = ({
size,
custom,
}: AccomplishCheckMarkProps) => {
return (
<CheckMarkWrapper size={size}>
{custom ?? (
<polyline
points="7 54 35 78 92 7"
css={{
strokeWidth: 15,
stroke: "var(--color-accent-1)"
}}
/>
)}
</CheckMarkWrapper>
)
}
type FailedCheckMarkProps = {
size: number;
custom?: React.ReactNode;
}
const FailedCheckMark = ({
size,
custom,
}: FailedCheckMarkProps) => {
const x = 15;
const y = 100 - x;
const color = "var(--color-accent-2)";
return (
<CheckMarkWrapper size={size}>
{custom ?? (<>
<polyline
points={`${x} ${x} ${y} ${y}`}
css={{
strokeWidth: 15,
stroke: color,
}}
/>
<polyline
points={`${y} ${x} ${x} ${y}`}
css={{
strokeWidth: 15,
stroke: color,
}}
/>
{/* 삼각형은 어떻니 */}
{/* <polyline
points="50 0 0 90 90 90 50 0"
css={{
strokeWidth: 15,
stroke: "var(--color-accent-2)"
}}
/> */}
</>)}
</CheckMarkWrapper>
)
}
interface Props {
state: TaskState;
className?: string;
size?: number;
customAccomplishedSVG?: React.ReactNode;
customFailedSVG?: React.ReactNode;
}
export const TaskCheckbox = ({
state,
className,
customAccomplishedSVG,
customFailedSVG,
size = 15,
}: Props) => {
const isChecked = useMemo(() => state !== "un-checked", [state]);
return (
<div
className={bem("cbx", "", className)}
css={{
display: "inline-block",
position: "relative",
width: `${size}px`,
height: `${size}px`,
// checkbox style
border: "0.07em solid #c8ccd4",
borderColor: isChecked ? "transparent" : undefined,
borderRadius: "3px",
cursor: "pointer",
transition: "scale",
// transition
"& .fade-enter": {
transform: "scale(0)",
opacity: 0,
},
"& .fade-enter-active": {
transform: "scale(1)",
opacity: 1,
transition: "all 0.5s ease",
},
}}
>
<CSSTransition
in={state === "accomplished"}
key="accomplish"
timeout={500}
exit={false}
classNames="fade"
unmountOnExit
children={
<AccomplishCheckMark
size={size}
custom={customAccomplishedSVG}
/>
}
/>
<CSSTransition
in={state === "failed"}
key="failed"
timeout={500}
exit={false}
classNames="fade"
unmountOnExit
children={
<FailedCheckMark
size={size}
custom={customFailedSVG}
/>
}
/>
</div>
)
}

View file

@ -40,7 +40,7 @@ export default class DailyRoutinePlugin extends Plugin {
});
devRunner();
activateView(DailyRoutineObsidianView.VIEW_TYPE, 1);
setTimeout(() => activateView(DailyRoutineObsidianView.VIEW_TYPE, 1), 500);
}
onunload() {

View file

@ -1,15 +1,86 @@
/** @jsxImportSource @emotion/react */
import { Month } from "@shared/period/month";
import { NoteAchivementCalendar } from "@widgets/note-achivement";
import { NoteAchivementWidget } from "@widgets/note-achivement";
import TabNavItem from '@mui/material/Tab';
import TabNav from '@mui/material/Tabs';
import { useState } from "react";
import { useLeaf } from "@shared/view/use-leaf";
import { css } from "@emotion/react";
import { Icon } from "@shared/components/Icon";
import { RoutineAchivementWidget } from "@widgets/routine-achivement";
type AchivementType = "note" | "routine";
const tabCss = css({
boxShadow: "none !important",
backgroundColor: "transparent !important",
minHeight: "0 !important",
fontSize: "0.7em",
height: "50px",
width: "50%",
})
export interface AchivementPageProps {
month: Month;
}
export const AchivementPage = ({ month }: AchivementPageProps) => {
// FIXME: routine -> note로
const [type, setType] = useState<AchivementType>("routine");
const { view, leafBgColor } = useLeaf();
return (
<>
<NoteAchivementCalendar month={month} />
<TabNav
value={type}
className="dr-tabs"
scrollButtons={false}
centered
sx={{ borderTop: 1, borderColor: 'divider' }}
onChange={(e, value) => setType(value)}
css={{
zIndex: 5,
backgroundColor: leafBgColor,
minHeight: 0,
width: "100%",
}}
>
<TabNavItem
label="Note"
value={"note"}
icon={<Icon icon="notebook-pen" />}
iconPosition="start"
css={tabCss}
/>
<TabNavItem
label="Routine"
value={"routine"}
icon={<Icon icon="alarm-clock-check" />}
iconPosition="start"
css={tabCss}
/>
</TabNav>
<div css={{
display: "flex",
justifyContent: "center"
}}>
<div>
{type === "note" ?
<NoteAchivementWidget month={month} height={390} maxWidth={420} />
:
<RoutineAchivementWidget month={month} height={390} maxWidth={420} routineName={"🛏️ 이부자리 정리하기"} />
}
<div css={{
position: "fixed",
top: "470px",
left: 0,
right: 0,
bottom: 0,
backgroundColor: leafBgColor,
zIndex: 50,
}} />
</div>
</div>
</>
)
}

View file

@ -1,13 +1,27 @@
import clsx from "clsx";
/** @jsxImportSource @emotion/react */
import React, { useEffect, useState } from "react";
interface CircleProps {
color: string;
percentage: number;
transition: boolean;
transition?: boolean;
width?: string;
strokeWidth?: number;
className?: string;
rotate?: number;
fill?: string;
}
export const Circle = ({ color, percentage: propsPercentage, transition=true }: CircleProps) => {
export const Circle = ({
color,
percentage: propsPercentage,
transition = true,
width,
strokeWidth = 25,
className,
rotate,
fill,
}: CircleProps) => {
const [percentage, setPercentage] = useState(transition ? 0 : propsPercentage);
const r = 85;
@ -22,6 +36,15 @@ export const Circle = ({ color, percentage: propsPercentage, transition=true }:
}, [percentage, propsPercentage, transition]);
return (
<svg
width={width??"100%"}
data-percentage={percentage}
viewBox="0 0 200 200"
className={className}
css={{
transform: `rotate(${rotate??0}deg)`,
}}
>
<circle
style={{
position: "relative",
@ -32,10 +55,11 @@ export const Circle = ({ color, percentage: propsPercentage, transition=true }:
cy={100}
stroke={color}
strokeDasharray={circ}
strokeWidth={25}
strokeWidth={strokeWidth}
strokeDashoffset={strokePct}
strokeLinecap="butt"
fill="transparent"
></circle>
fill={fill ?? "transparent"}
/>
</svg>
);
};

View file

@ -0,0 +1,19 @@
export const getCustomAccentHSL = ({
h = 1,
s = 1,
l = 1,
a = 1,
}: {
h?: number;
s?: number;
l?: number;
a?: number;
}) => {
return `hsla(calc(var(--accent-h) * ${a}), calc(var(--accent-s) * ${s}), calc(var(--accent-l) * ${l}), ${a})`;
}

View file

@ -11,12 +11,12 @@ import { OnArgs } from "react-calendar/dist/cjs/shared/types";
export interface CalendarNavigationProps {
month: Month;
onMonthChange?: (month: Month) => void;
setMonth: (month: Month) => void;
className?: string;
}
export const CalendarNavigation = ({
month,
onMonthChange,
setMonth,
className,
}: CalendarNavigationProps) => {
const { leafBgColor } = useLeaf();
@ -28,13 +28,12 @@ export const CalendarNavigation = ({
calendarRef.current.setActiveStartDate(month.startDay.getJsDate());
}, [month]);
const onActiveStartDateChange = useCallback(({ activeStartDate, view }: OnArgs) => {
if(!activeStartDate || view !== "month") return;
const changedMonth = Month.fromJsDate(activeStartDate);
if(!changedMonth.startDay.isSameMonth(month.startDay)){
onMonthChange?.(changedMonth);
}
}, [month.startDay, onMonthChange]);
const lastChangedMonthRef = useRef<Month>(month);
const monthChangeHandler = useCallback((month: Month) => {
if(lastChangedMonthRef.current.isSameMonth(month)) return;
setMonth?.(month);
lastChangedMonthRef.current = month;
}, [setMonth]);
const calendarStyles = useMemo(() => css({
// 네비게이션
@ -48,7 +47,7 @@ export const CalendarNavigation = ({
// 캘린더 + 월, 년 선택 컨테이너
".react-calendar__viewContainer": {
position: "relative",
zIndex: 10,
zIndex: 10000,
"div": {
position: "absolute",
@ -117,7 +116,15 @@ export const CalendarNavigation = ({
css={calendarStyles}
defaultValue={month.startDay.getJsDate()}
allowPartialRange={false}
onActiveStartDateChange={onActiveStartDateChange}
onActiveStartDateChange={({ activeStartDate, view }: OnArgs) => {
if(!activeStartDate || view !== "month") return;
const newMonth = Month.fromJsDate(activeStartDate);
monthChangeHandler(newMonth);
}}
onClickMonth={value => {
const month = Month.fromJsDate(value);
monthChangeHandler(month);
}}
selectRange={false}
showNavigation={true}
/>

View file

@ -1,7 +1,7 @@
/** @jsxImportSource @emotion/react */
import { VirtualSwiper } from "@shared/components/VirtualSwiper";
import { Month } from "@shared/period/month";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { CalendarNavigation } from "./Nav";
@ -29,20 +29,28 @@ interface Props {
month: Month;
children: (month: Month, index?: number) => React.ReactNode;
verticalHeight?: number;
maxWidth?: number
}
export const SwipeableCalendar = ({
month: propsMonth,
children,
verticalHeight,
maxWidth,
}: Props) => {
const [months, setMonths] = useState<Month[]>(loadMonths(propsMonth));
const [activeMonth, setActiveMonth] = useState<Month>(propsMonth);
const resetMonths = useCallback((month: Month) => {
console.log("resetMonths");
setActiveMonth(month);
setMonths(loadMonths(month));
}, []);
useEffect(() => {
console.log(activeMonth.monthNum);
console.log(months.map(m => m.monthNum));
}, [activeMonth, months]);
const onSlideChange = useCallback((month: Month) => {
setActiveMonth(month);
}, []);
@ -51,10 +59,11 @@ export const SwipeableCalendar = ({
<div css={{
position: 'relative',
height: '100%',
maxWidth,
}}>
<CalendarNavigation
month={activeMonth}
onMonthChange={resetMonths}
setMonth={resetMonths}
/>
<VirtualSwiper
datas={months}

View file

@ -32,7 +32,7 @@ export class Day {
}
}
static now(): Day{
static today(): Day{
return new Day(moment());
}
@ -159,6 +159,7 @@ export class Day {
}
}
// FIXME: 이건 Month에 있어야 맞지않냐
daysInMonth(){
return this.#moment.daysInMonth();
}

View file

@ -16,7 +16,7 @@ export class Month {
}
static now() {
return new Month(Day.now());
return new Month(Day.today());
}
get startDay() {
@ -31,6 +31,10 @@ export class Month {
return this.#startDay.month;
}
isSameMonth(other: Month){
return this.#startDay.isSameMonth(other.startDay);
}
add_cpy(amount: number) {
return new Month(
this.#startDay.clone(m => m.add(amount, "month"))

View file

@ -1,4 +1,5 @@
import { DrTabType } from "@shared/tab/use-tab-route";
export const INITIAL_TAB: DrTabType = "note";
// FIXME: achivement변경하기
export const INITIAL_TAB: DrTabType = "achivement";

View file

@ -1,10 +1,10 @@
/** @jsxImportSource @emotion/react */
import { Day } from "@shared/period/day";
import { Tile } from "../model/load-tile-map";
import { Day, DayOfWeek } from "@shared/period/day";
import { Tile } from "../model/types";
import { TEXT_CSS } from "@shared/components/text-style";
import { Task } from "@entities/note";
import { css } from "@emotion/react";
import { useContext } from "react";
import { useContext, useEffect } from "react";
import { useTileHeightInfo } from "./tile-height-info-context";
@ -15,6 +15,8 @@ const defaultTextStyle = css({
const DateBadge = ({ day }: { day: Day }) => {
const isWeekend = day.isSameDow(DayOfWeek.SAT) || day.isSameDow(DayOfWeek.SUN);
return (
<div css={[{
position: 'relative',
@ -30,8 +32,7 @@ const DateBadge = ({ day }: { day: Day }) => {
borderRadius: '50%',
textAlign: 'start',
backgroundColor: 'transparent',
// textAlign: day.isToday() ? 'center' : 'start',
// backgroundColor: day.isToday() ? 'var(--color-accent)' : 'transparent',
color: isWeekend ? 'var(--color-accent-1)' : "var(--text-color)",
}}>
{day.date}
</span>
@ -47,7 +48,9 @@ const TaskLineContainer = ({ tasks }: { tasks: Task[] }) => {
const { limitedTaskPer } = useTileHeightInfo();
if(tasks.length <= limitedTaskPer){
return (
<div>
<div className="dr-calendar-tasks" css={{
width: '100%',
}}>
{tasks.map(task => (
<TaskLine
key={task.name}
@ -59,7 +62,9 @@ const TaskLineContainer = ({ tasks }: { tasks: Task[] }) => {
}
return (
<div className="dr-calendar-tasks">
<div className="dr-calendar-tasks" css={{
width: '100%',
}}>
{tasks.slice(0, limitedTaskPer - 1).map(task => (
<TaskLine
key={task.name}
@ -88,7 +93,6 @@ const TaskLine = ({ task }: { task: Task }) => {
borderRadius: '2px',
textAlign: 'start',
textOverflow: 'ellipsis',
overflow: "hidden",
whiteSpace: "nowrap",
}]}>
{task.name}

View file

@ -1,14 +1,6 @@
/** @jsxImportSource @emotion/react */
import { Month } from "@shared/period/month";
import { useCallback, useMemo, useState } from "react";
import { useLeaf } from "@shared/view/use-leaf";
import { SwipeableCalendar } from "@shared/components/swipeable-calender/SwipeableCalendar";
import { useAsync } from "@shared/utils/use-async";
import { loadTileMap } from "../model/load-tile-map";
import { useTabRoute } from "@shared/tab/use-tab-route";
import { Day } from "@shared/period/day";
import { CalendarTile } from "./CalendarTile";
import { Task } from "@entities/note";
import { useTabHeight } from "@app/ui/use-tab-height";
import { TileHeightInfoProvider } from "./tile-height-info-context";
import { CalendarSlide } from "./CalendarSlide";

View file

@ -1 +1 @@
export { NoteAchivementCalendar } from "./ui/NoteAchivementCalendar";
export { NoteAchivementWidget } from "./ui/NoteAchivementWidget";

View file

@ -7,6 +7,7 @@ import { useAsync } from "@shared/utils/use-async";
import { useCallback, useMemo } from "react";
import { useTabRoute } from "@shared/tab/use-tab-route";
import { PerformanceCircle } from "@features/performance";
import { Badge } from "@mui/material";
interface CalendarSlideProps {
@ -16,18 +17,33 @@ export const CalendarSlide = ({ month }: CalendarSlideProps) => {
const notesAsync = useAsync(async () => await noteRepository.loadBetween(month.startDay, month.endDay), [month]);
const route = useTabRoute(s=>s.route);
const tile = useCallback((tileDay: Day) => {
const tile = useCallback((day: Day) => {
if(day.month !== month.monthNum) return <></>;
let performance = NoteEntity.getEmptyNotePerformance();
if(notesAsync.value){
const note = notesAsync.value.find(note => note.day.isSameDay(tileDay));
const note = notesAsync.value.find(note => note.day.isSameDay(day));
if(note){
performance = NoteEntity.getPerformance(note);
}
}
return (
<PerformanceCircle performance={performance} text={tileDay.date.toString()} />
<div css={{
width: "100%",
height: "100%",
}}>
<Badge badgeContent="Tdy" color="primary" overlap="circular" invisible={!day.isToday()} css={{
"& > .MuiBadge-badge": {
padding: "2px 5px",
height: "fit-content",
backgroundColor: "var(--color-accent)",
clipPath: "inset(-50px)"
}
}}>
<PerformanceCircle performance={performance} text={day.date.toString()} />
</Badge>
</div>
)
}, [notesAsync.value]);
}, [month.monthNum, notesAsync.value]);
const onTileClick = useCallback((day: Day) => {
@ -54,7 +70,8 @@ export const CalendarSlide = ({ month }: CalendarSlideProps) => {
gap: "0",
},
tile: {
border: "0"
border: "0",
overflow: "visible !important",
}
}}
/>

View file

@ -1,56 +0,0 @@
/** @jsxImportSource @emotion/react */
import { NoteEntity, noteRepository, RoutineNote } from '@entities/note';
import { Day } from "@shared/period/day";
import { BaseCalendar } from '@shared/components/BaseCalendar';
import { useCallback, useEffect, useState } from "react";
import { useTabRoute } from '@shared/tab/use-tab-route';
import { Month } from '@shared/period/month';
import { PerformanceCircle } from '@features/performance';
import { SwipeableCalendar } from '@shared/components/swipeable-calender/SwipeableCalendar';
import { useAsync } from '@shared/utils/use-async';
import { useTabHeight } from '@app/ui/use-tab-height';
export interface NoteAchivementCalendarProps {
month: Month;
}
export const NoteAchivementCalendar = ({ month }: NoteAchivementCalendarProps) => {
const notesAsync = useAsync(async () => await noteRepository.loadBetween(month.startDay, month.endDay), [month]);
const route = useTabRoute(s=>s.route);
const tabHeight = useTabHeight();
const tile = useCallback((tileDay: Day) => {
let performance = NoteEntity.getEmptyNotePerformance();
if(notesAsync.value){
const note = notesAsync.value.find(note => note.day.isSameDay(tileDay));
if(note){
performance = NoteEntity.getPerformance(note);
}
}
return (
<PerformanceCircle performance={performance} text={tileDay.date.toString()} />
)
}, [notesAsync.value]);
const onTileClick = useCallback((day: Day) => {
route("note", { day });
}, [route]);
if(notesAsync.loading) return <div>Loading...</div>;
return (
<SwipeableCalendar
month={month}
onTileClick={onTileClick}
tile={tile}
verticalHeight={tabHeight}
stylesOption={{
tile: {
border: "0",
}
}}
/>
)
}

View file

@ -0,0 +1,24 @@
/** @jsxImportSource @emotion/react */
import { SwipeableCalendar } from '@shared/components/swipeable-calender/SwipeableCalendar';
import { Month } from '@shared/period/month';
import { CalendarSlide } from './CalendarSlide';
export interface NoteAchivementCalendarProps {
month: Month;
height: number;
maxWidth: number;
}
export const NoteAchivementWidget = ({ month, height, maxWidth }: NoteAchivementCalendarProps) => {
return (
<SwipeableCalendar
month={month}
verticalHeight={height}
maxWidth={maxWidth}
>
{month => <CalendarSlide month={month}/>}
</SwipeableCalendar>
)
}

View file

@ -0,0 +1,2 @@
export { RoutineAchivementWidget } from "./ui/RoutineAchivementWidget";

View file

@ -0,0 +1,43 @@
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<TileMap> => {
const notes = await noteRepository.loadBetween(month.startDay, month.endDay);
const tileMap = new Map<DayFormat, Tile>();
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;
}

View file

@ -0,0 +1,10 @@
import { NoteEntity, RoutineNote, TaskState } from "@entities/note";
import { Day, DayFormat } from "@shared/period/day";
import { Month } from "@shared/period/month";
export type TileMap = Map<DayFormat, Tile>;
export type Tile = {
day: Day;
state: TaskState | "inactive";
}

View file

@ -0,0 +1,57 @@
/** @jsxImportSource @emotion/react */
import { noteRepository } from "@entities/note";
import { BaseCalendar } from "@shared/components/BaseCalendar";
import { Day } 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 { Tile } from "../model/types";
import { resolveRoutineTiles } from "../model/resolve-routine-tiles";
interface Props {
month: Month;
routineName: string;
}
export const CalendarSlide = ({ routineName, month }: Props) => {
const tilesAsync = useAsync(async () => await resolveRoutineTiles(routineName, month), [month]);
const tile = useCallback((day: Day) => {
if(day.month !== month.monthNum) return <></>;
const tile = tilesAsync.value?.get(day.format());
if(!tile) throw new Error(`tile not found for ${day.format()}`);
return <CalendarTile tile={tile} />;
}, [month.monthNum, tilesAsync.value]);
const onTileClick = useCallback((day: Day) => {
console.log(day);
}, []);
const tileDisabled = useCallback((day: Day) => {
return day.month !== month.monthNum;
}, [month]);
if(tilesAsync.loading) return <div>Loading...</div>;
return (
<BaseCalendar
month={month}
setMonth={() => {}}
onTileClick={onTileClick}
tile={tile}
showNavigation={false}
tileDisabled={tileDisabled}
styleOptions={{
tileContainer: {
gap: "0",
},
tile: {
border: "0",
overflow: "visible !important",
}
}}
/>
);
};

View file

@ -0,0 +1,76 @@
/** @jsxImportSource @emotion/react */
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;
}
export const CalendarTile = ({
tile,
}: Props) => {
const isActive = tile.state !== "inactive";
const isTodayOrBefore = tile.day.isSameOrBefore(Day.today());
const color = isActive && isTodayOrBefore ? "var(--color-accent)" : "#ececec";
const opacity = isActive && isTodayOrBefore ? 1 : 0.5;
return (
<div css={{
position: "relative",
}}>
<Badge badgeContent="Tdy" color="primary" overlap="circular" invisible={!tile.day.isToday()} css={{
"& > .MuiBadge-badge": {
padding: "2px 5px",
height: "fit-content",
}
}}>
<Circle
color={color}
css={{opacity}}
percentage={100}
rotate={-90}
strokeWidth={10}
/>
</Badge>
<div css={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
opacity,
}}>
<div>{tile.day.date}</div>
<div css={{
fontSize: "1.2em",
textAlign: "center",
}}>
{ tile.state === "inactive" || !isTodayOrBefore
?
<TaskCheckbox
state={"un-checked"}
css={{
visibility: "hidden",
}}
size={14}
/>
:
<TaskCheckbox
state={tile.state}
size={14}
/>
}
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,49 @@
/** @jsxImportSource @emotion/react */
import { Month } from '@shared/period/month';
import { SwipeableCalendar } from '@shared/components/swipeable-calender/SwipeableCalendar';
import { CalendarSlide } from './CalendarSlide';
import { TEXT_CSS } from '@shared/components/text-style';
export interface Props {
month: Month;
routineName: string;
height: number;
maxWidth: number;
}
export const RoutineAchivementWidget = ({
month,
height,
routineName,
maxWidth
}: Props) => {
return (
<div css={{
position: "relative",
}}>
<SwipeableCalendar
month={month}
verticalHeight={height}
maxWidth={maxWidth}
>
{month => <CalendarSlide month={month} routineName={routineName} />}
</SwipeableCalendar>
<div css={{
position: "absolute",
top: height,
transform: "translateY(100%)",
marginTop: "1em",
left: 0,
zIndex: 1000,
borderTop: "1px solid var(--color-base-30)",
width: "100%",
}}>
<div css={[TEXT_CSS.medium, {
padding: "1em 1em",
}]}>{routineName}</div>
</div>
</div>
)
}

View file

@ -12,7 +12,7 @@ export const createNewRoutine = (): Routine => {
showOnCalendar: false,
activeCriteria: "week",
daysOfWeek: Day.getDaysOfWeek(),
daysOfMonth: [Day.now().date],
daysOfMonth: [Day.today().date],
enabled: true
}
}

View file

@ -34,7 +34,7 @@ export const DayNodeComponent = React.memo(({ dayNode: { day, performance: _perf
}, [isActiveDay, activeNode.performance]);
const afterStyle = useMemo(() => {
if(!day.isAfter(Day.now())) return css({});
if(!day.isAfter(Day.today())) return css({});
return css({
content: "''",
position: "absolute",

View file

@ -21,7 +21,7 @@ export const WeeksWidget = ({ className }: WeeksProps) => {
const { note, setNote } = useRoutineNote();
const activeDay = useMemo(() => note.day, [note]);
const activeWeek = useMemo(() => new Week(activeDay), [activeDay]);
const currentDayPercentage = useMemo(() => NoteEntity.getPerformance(note), [note]);
const currentNotePerformance = useMemo(() => NoteEntity.getPerformance(note), [note]);
const [ weeks, setWeeks ] = useState<WeekNode[]>([]);
const { leafBgColor } = useLeaf();
@ -57,7 +57,7 @@ export const WeeksWidget = ({ className }: WeeksProps) => {
return (
<WeeksActiveDayContextProvider
data={{ day: activeDay, performance: currentDayPercentage }}
data={{ day: activeDay, performance: currentNotePerformance }}
onDataChange={(store, data) => store.setState(data)}
>
<VirtualSwiper