일주일 선택 뷰 제작

This commit is contained in:
sechan100 2024-10-02 11:06:10 +09:00
parent 934d14c767
commit b8adaa5ecd
13 changed files with 363 additions and 65 deletions

46
package-lock.json generated
View file

@ -10,10 +10,12 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"clsx": "^2.1.1",
"i": "^0.3.7",
"npm": "^10.8.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"zustand": "^5.0.0-rc.2"
},
"devDependencies": {
"@types/lodash": "^4.17.7",
@ -579,13 +581,13 @@
"version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"dev": true
"devOptional": true
},
"node_modules/@types/react": {
"version": "18.3.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
"integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==",
"dev": true,
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@ -951,6 +953,14 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -997,7 +1007,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true
"devOptional": true
},
"node_modules/debug": {
"version": "4.3.6",
@ -4652,6 +4662,34 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zustand": {
"version": "5.0.0-rc.2",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.0-rc.2.tgz",
"integrity": "sha512-o2Nwuvnk8vQBX7CcHL8WfFkZNJdxB/VKeWw0tNglw8p4cypsZ3tRT7rTRTDNeUPFS0qaMBRSKe+fVwL5xpcE3A==",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
}
}
}

View file

@ -26,9 +26,11 @@
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"clsx": "^2.1.1",
"i": "^0.3.7",
"npm": "^10.8.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"zustand": "^5.0.0-rc.2"
}
}

View file

@ -5,6 +5,7 @@ import { Day } from "lib/day";
import { routineManager, Routine } from "./routine";
import { FileNotFoundError } from "lib/file/errors";
import { parseProperties } from "lib/file/utils";
import moment from "moment";
export interface Task {
@ -24,11 +25,10 @@ export interface RoutineNote {
export const routineNoteArchiver = {
/**
* RoutineNote를 archive에서 .
* day RoutineNote를 archive에서 .
* archive에 .
*/
async getTodayRoutineNote(): Promise<RoutineNote> {
const day = Day.now();
async getRoutineNote(day: Day): Promise<RoutineNote> {
try {
const file = archiveDAO.getRoutineNoteFile(day);
return archiveDAO.parseFile(file);
@ -101,9 +101,8 @@ const archiveDAO = {
return { name, checked };
});
console.log(tasks)
return {
day: new Day(props.day),
day: new Day(moment(props.day)),
tasks
};
},
@ -161,7 +160,7 @@ const createNewRoutineNote = async (day: Day): Promise<RoutineNote> => {
* routine으로부터 Task를 .
* @param routine
* @param day
* @returns RoutineTask | null routine이 day에 null을 .
* @returns RoutineTask | null routine이 day에 null을 .
*/
const deriveRoutineTask = (routine: Routine, day: Day): RoutineTask | null => {
if (!routine.properties.dayOfWeeks.contains(day.getDayOfWeek())) return null;

View file

@ -0,0 +1,33 @@
import { Day } from "lib/day";
import { useRoutineNote } from "./use-routine-note";
import moment from "moment";
import { ButtonComponent } from "obsidian";
import clsx from "clsx";
export const DaysMenu = () => {
const routineNote = useRoutineNote(s=>s.routineNote);
const setRoutineNote = useRoutineNote(s=>s.setRoutineNote);
const today = Day.now();
return (
<div className="dr-days">
<div className="dr-days__flex">
{today.getCurrentWeek().map((day, idx) => {
return (
<button key={idx} className={clsx("dr-days__item", {
"dr-days__today": day.isSameDay(today),
"dr-days__current": day.isSameDay(routineNote.day)
})} onClick={() => {
setRoutineNote(day);
}}>
<div>{day.format("ddd")}</div>
<div>{day.format("M/D")}</div>
</button>
)
})}
</div>
</div>
)
}

View file

@ -1,43 +1,32 @@
import { RoutineNote, routineNoteArchiver, RoutineTask } from "entities/archive";
import { RoutineComponent } from "./routine";
import React, { useCallback } from "react";
import { RoutineNote, routineNoteArchiver } from "entities/archive";
import React, { createContext, useContext, useEffect, useRef, useState } from "react";
import "./style.css";
import { routineManager } from "entities/routine";
import { NoteView } from "./note";
import { DaysMenu } from "./days";
import { Day } from "lib/day";
import { useRoutineNote, UseRoutineNoteContext } from "./use-routine-note";
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Props {
routineNote: RoutineNote
}
export const RoutineNoteView = ({ routineNote }: Props) => {
export const RoutineNoteView = () => {
const todayRoutineNoteRef = useRef<RoutineNote>({
day: Day.now(),
tasks: [],
});
// 루틴 체크, 혹은 체크해제시 콜백
const onCheckChange = useCallback((routine: RoutineTask, checked: boolean) => {
// 개별루틴 업데이트
routineManager.updateAchievement({
routineName: routine.name,
day: routineNote.day,
checked
useEffect(() => {
routineNoteArchiver.getRoutineNote(Day.now())
.then(note => {
todayRoutineNoteRef.current = note;
});
// 루틴노트 업데이트
routineNoteArchiver.updateTaskCheck(routineNote, routine.name, checked);
}, [routineNote]);
}, []);
return (
<div className="dr-note">
<header className="dr-note__header">
<h1>Daily Routine</h1>
<span>{routineNote.day.getAsUserCustomFormat()}</span>
</header>
{routineNote.tasks.map((task, idx) => {
return (
<div key={idx}>
<RoutineComponent onCheckChange={onCheckChange} task={task} />
</div>
)
})}
</div>
);
}
<UseRoutineNoteContext initialData={todayRoutineNoteRef.current}>
<DaysMenu />
<NoteView />
</UseRoutineNoteContext>
);
}

View file

@ -0,0 +1,43 @@
import { RoutineNote, RoutineTask, routineNoteArchiver } from "entities/archive";
import { routineManager } from "entities/routine";
import { useCallback } from "react";
import { RoutineComponent } from "./routine";
import { useRoutineNote } from "./use-routine-note";
export const NoteView = () => {
const routineNote = useRoutineNote(s=>s.routineNote);
console.log(routineNote);
// 루틴 체크, 혹은 체크해제시 콜백
const onCheckChange = useCallback((routine: RoutineTask, checked: boolean) => {
// 개별루틴 업데이트
routineManager.updateAchievement({
routineName: routine.name,
day: routineNote.day,
checked
});
// 루틴노트 업데이트
routineNoteArchiver.updateTaskCheck(routineNote, routine.name, checked);
}, [routineNote]);
return (
<div className="dr-note">
<header className="dr-note__header">
<h1>Daily Routine</h1>
<span>{routineNote.day.getAsUserCustomFormat()}</span>
</header>
{routineNote.tasks.map((task, idx) => {
return (
<div key={idx}>
<RoutineComponent onCheckChange={onCheckChange} task={task} />
</div>
)
})}
</div>
);
}

View file

@ -1,4 +1,4 @@
import React from "react"
import React, { useState } from "react"
import { RoutineTask } from "entities/archive";
@ -8,6 +8,7 @@ interface Props {
onCheckChange: (routine: RoutineTask, checked: boolean) => void;
}
export const RoutineComponent = ({ task: routine, onCheckChange }: Props) => {
const [checked, setChecked] = useState(routine.checked);
const id = `routine-${routine.name}`
@ -16,7 +17,10 @@ export const RoutineComponent = ({ task: routine, onCheckChange }: Props) => {
{/* 전체적으로 감싸주는 label */}
<label htmlFor={id} className="dr-routine__item">
{/* 체크박스(hidden) */}
<input onChange={(e) => onCheckChange(routine, e.target.checked)} type="checkbox" id={id} className="hidden"/>
<input checked={checked} onChange={(e) => {
onCheckChange(routine, e.target.checked)
setChecked(e.target.checked)
}} type="checkbox" id={id} className="hidden"/>
{/* 체크박스(display) */}
<label htmlFor={id} className="dr-routine__cbx">
<svg viewBox="0 0 14 12">

View file

@ -21,6 +21,56 @@
}
/* #####################################################################
* ########################## Days Menu ################################
* ##################################################################### */
.dr-days {
display: block;
width: 100%;
overflow-x: scroll;
scrollbar-width: none;
padding-top: 5px;
}
.dr-days__flex {
display: flex;
justify-content: space-between;
align-items: center;
gap: 5px;
flex-wrap: nowrap;
}
.dr-days__item {
display: inline-block;
position: relative;
height: auto;
padding: 0.5em 1em;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.dr-days__item:hover {
background-color: #f5f6f8;
}
.dr-days__current {
position: relative;
font-size: 1.02em;
padding: 0.6em 1.1em;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1) !important;
border: 2px solid #c8ccd4;
border-radius: 7px;
top: -5px;
transition: top 0.5s ease;
}
.dr-days__today {
background-color: hsla(var(--color-accent-1-hsl), 1) !important;
}
/* #####################################################################
* ####################### ROUTINE COMPONENT ###########################
* ##################################################################### */

View file

@ -0,0 +1,20 @@
import { RoutineNote, routineNoteArchiver } from "entities/archive";
import { createStoreContext } from "lib/create-store-context";
import { Day } from "lib/day";
import { get } from "lodash";
import { create } from "zustand";
interface UseRoutineNote {
routineNote: RoutineNote;
setRoutineNote: (day: Day) => Promise<void>;
}
export const {useStoreHook: useRoutineNote, context: UseRoutineNoteContext} = createStoreContext<RoutineNote, UseRoutineNote>((data, set) => ({
routineNote: data,
setRoutineNote: async (day: Day) => {
const routineNote = await routineNoteArchiver.getRoutineNote(day);
set({routineNote});
},
}));

View file

@ -0,0 +1,74 @@
/**
* useContext와 zustand store를 .
* zustand store를 , .
*/
import { StoreApi, useStore } from 'zustand';
import { createStore } from 'zustand/vanilla';
import { createContext, useContext, useEffect, useRef } from 'react';
import React from 'react';
/**
* D: 초기
* S: store
*/
type SetState<S> = (partial: S | Partial<S> | ((state: S) => S | Partial<S>), replace?: boolean | undefined) => void
type GetState<S> = () => S
type Initializer<D, S> = (data: D, set: SetState<S>, get: GetState<S>) => S
// 해당 함수는 '(초기데이터, set, get) => store' 형태의 콜백을 받아서 초기데이터를 기반으로 store를 생성해주는 함수를 반환한다.
const createStoreWithInit: <D, S>(initializer: Initializer<D, S>) => (data: D) => StoreApi<S>
= (initializer) => {
return (data) => createStore((set, get) => initializer(data, set, get));
}
interface StoreContextProviderProps<D, S> {
initialData: D;
onDataChanged?: (state: S, data: D) => void;
children: React.ReactNode;
}
export const createStoreContext = <D, S>(initializer: Initializer<D, S>): {
context: React.FC<StoreContextProviderProps<D, S>>,
useStoreHook: <R>(selector: (state: S) => R) => R;
} => {
const StoreContext = createContext<StoreApi<S> | null>(null);
const store = createStoreWithInit<D, S>(initializer);
/**
* Provider initialData는 store .
* onDataChanged .
*
* onDataChanged가 , initialData가 context에서 store .
* .
*/
const Provider = React.memo(function Provider({ initialData, onDataChanged, children }: StoreContextProviderProps<D, S>) {
const storeRef = useRef(store(initialData));
useEffect(() => {
if(onDataChanged){
onDataChanged(storeRef.current.getState(), initialData);
}
}, [initialData, onDataChanged]);
return <StoreContext.Provider value={storeRef.current}>{children}</StoreContext.Provider>;
}, (prev, next) => {
// onDataChanged가 정의되었다면 컴포넌트를 절대 리렌더링되지 않는다.
if(prev.onDataChanged){
return true;
// onDataChanged가 정의되지 않았다면, initialData가 변경될 때마다 리렌더링된다.
} else {
return prev.initialData !== next.initialData;
}
});
const useStoreHook: <R>(selector:(state: S) => R) => R
= (selector) => {
const store = useContext(StoreContext);
if (!store) throw new Error("해당 context에서는 store가 정의되지 않습니다. 올바른 컨텍스트에서 접근가능");
return useStore(store, selector);
}
return { context: Provider, useStoreHook };
}

View file

@ -21,17 +21,35 @@ export class Day {
*
* @param day "YYYY-MM-DD"
*/
constructor(day: string) {
const m = moment(day);
constructor(_moment: moment.Moment) {
const m = moment(_moment);
if(m.isValid()) {
this.moment = m;
} else {
throw new Error(`Invalid date format. ${day}`);
throw new Error(`Invalid date format. ${_moment.toString()}`);
}
}
static now(): Day{
return new Day(moment().format(Day.defaultFormat));
return new Day(moment());
}
format(format: string){
return this.moment.format(format);
}
/**
*
*/
getCurrentWeek(){
const week = [];
const m = moment(this.moment);
for(let i = 0; i < 7; i++) {
const day = new Day(m.startOf('week').add(i, 'd'));
week.push(day);
}
return week;
}
getAsUserCustomFormat(){
@ -57,7 +75,7 @@ export class Day {
}
isSameDay(day: Day){
return this.moment.isSame(day.moment);
return this.moment.isSame(day.moment, 'day');
}
isSameDayOfWeek(day: Day){

View file

@ -30,21 +30,10 @@ export class DailyRoutineView extends ReactView {
* ROOT PAGE
*/
const DailyRoutineViewComponent = () => {
const [routineNote, setRoutineNote] = useState<RoutineNote>({
tasks: [],
day: Day.now()
});
useEffect(() => {
routineNoteArchiver.getTodayRoutineNote()
.then(routineNote => {
setRoutineNote(routineNote);
});
}, []);
return (
<div>
<RoutineNoteView routineNote={routineNote} />
<RoutineNoteView />
</div>
);
}

File diff suppressed because one or more lines are too long