mirror of
https://github.com/sechan100/daily-routine-2.git
synced 2026-07-22 05:37:51 +00:00
debug: 캘린더 nav로 month 변경할 수 없는 문제를 해결
This commit is contained in:
parent
d4600c66d8
commit
0e2df22983
40 changed files with 691 additions and 290 deletions
|
|
@ -1,3 +1,3 @@
|
|||
node_modules/
|
||||
|
||||
**/dist/**
|
||||
main.js
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 번들 파일
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export const RouitneNoteTab = () => {
|
|||
}
|
||||
return params;
|
||||
} else {
|
||||
return { day: Day.now() };
|
||||
return { day: Day.today() };
|
||||
}
|
||||
}, [routeParams, tab]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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%"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
170
src/features/task-el/ui/TaskCheckbox.tsx
Normal file
170
src/features/task-el/ui/TaskCheckbox.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ export default class DailyRoutinePlugin extends Plugin {
|
|||
});
|
||||
|
||||
devRunner();
|
||||
activateView(DailyRoutineObsidianView.VIEW_TYPE, 1);
|
||||
setTimeout(() => activateView(DailyRoutineObsidianView.VIEW_TYPE, 1), 500);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
19
src/shared/components/obsidian-accent-color.ts
Normal file
19
src/shared/components/obsidian-accent-color.ts
Normal 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})`;
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { NoteAchivementCalendar } from "./ui/NoteAchivementCalendar";
|
||||
export { NoteAchivementWidget } from "./ui/NoteAchivementWidget";
|
||||
|
|
@ -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",
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
24
src/widgets/note-achivement/ui/NoteAchivementWidget.tsx
Normal file
24
src/widgets/note-achivement/ui/NoteAchivementWidget.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
2
src/widgets/routine-achivement/index.ts
Normal file
2
src/widgets/routine-achivement/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
export { RoutineAchivementWidget } from "./ui/RoutineAchivementWidget";
|
||||
|
|
@ -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;
|
||||
}
|
||||
10
src/widgets/routine-achivement/model/types.ts
Normal file
10
src/widgets/routine-achivement/model/types.ts
Normal 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";
|
||||
}
|
||||
57
src/widgets/routine-achivement/ui/CalendarSlide.tsx
Normal file
57
src/widgets/routine-achivement/ui/CalendarSlide.tsx
Normal 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",
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
76
src/widgets/routine-achivement/ui/CalendarTile.tsx
Normal file
76
src/widgets/routine-achivement/ui/CalendarTile.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue