mirror of
https://github.com/sechan100/daily-routine-2.git
synced 2026-07-22 05:37:51 +00:00
일주일 선택 뷰 제작
This commit is contained in:
parent
934d14c767
commit
b8adaa5ecd
13 changed files with 363 additions and 65 deletions
46
package-lock.json
generated
46
package-lock.json
generated
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
33
src/features/routine-note/days.tsx
Normal file
33
src/features/routine-note/days.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
43
src/features/routine-note/note.tsx
Normal file
43
src/features/routine-note/note.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 ###########################
|
||||
* ##################################################################### */
|
||||
|
|
|
|||
20
src/features/routine-note/use-routine-note.ts
Normal file
20
src/features/routine-note/use-routine-note.ts
Normal 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});
|
||||
},
|
||||
}));
|
||||
74
src/lib/create-store-context.tsx
Normal file
74
src/lib/create-store-context.tsx
Normal 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 };
|
||||
}
|
||||
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
41
styles.css
41
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue