Compare commits

...

3 Commits

Author SHA1 Message Date
2a7d41bba5 feat: sort tasks by date 2025-04-22 12:46:45 +03:00
d7c406a930 feat: calendar on create 2025-04-22 12:39:34 +03:00
7a98dbfe91 feat: modal calendar in edit tasks 2025-04-22 12:31:55 +03:00
3 changed files with 227 additions and 154 deletions

View File

@@ -1,13 +1,18 @@
import { cn } from "@/utils/class-merge"; import { cn } from "@/utils/class-merge";
import { ClockIcon } from "@heroicons/react/24/outline";
import { FunctionComponent } from "preact"; import { FunctionComponent } from "preact";
import { Dispatch, StateUpdater } from "preact/hooks"; import { Dispatch, StateUpdater, useState } from "preact/hooks";
import { Calendar } from "primereact/calendar"; import { Calendar, CalendarPassThroughMethodOptions } from "primereact/calendar";
import { FormEvent } from "primereact/ts-helpers";
import Button from "./ui/Button";
import ModalWindow from "./ui/Modal"; import ModalWindow from "./ui/Modal";
interface ModalCalendarProps { interface ModalCalendarProps {
isOpen?: boolean; isOpen?: boolean;
setIsOpen?: Dispatch<StateUpdater<boolean>>; setIsOpen?: Dispatch<StateUpdater<boolean>>;
onClose?: () => void; onClose?: () => void;
value?: Date;
onChange?: (e: FormEvent<Date>) => void;
} }
const TRANSITIONS = { const TRANSITIONS = {
@@ -22,143 +27,174 @@ const TRANSITIONS = {
}, },
}; };
const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpen, onClose }) => { const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpen, onClose, onChange, value }) => {
const [showTime, setShowTime] = useState(false);
return ( return (
<ModalWindow isOpen={isOpen} setIsOpen={setIsOpen} onClose={onClose} className="md:h-[40rem] md:w-[30rem]"> <ModalWindow
<Calendar isOpen={isOpen}
inline setIsOpen={setIsOpen}
pt={{ onClose={() => {
root: ({ props }) => ({ onClose!();
className: cn("inline-flex max-w-full relative", { setShowTime(false);
"opacity-60 select-none pointer-events-none cursor-default": props.disabled, }}
className="md:h-[40rem] md:w-[30rem]"
zIndex={60}
>
<div class="w-full flex-1 self-start">
<Calendar
inline
onChange={onChange}
value={value}
hourFormat="24"
showTime={showTime}
pt={{
root: ({ props }: CalendarPassThroughMethodOptions) => ({
className: cn("inline-flex w-full relative", {
"opacity-60 select-none pointer-events-none cursor-default": props.disabled,
}),
}), }),
}), input: ({ props }: CalendarPassThroughMethodOptions) => ({
input: ({ props }) => ({ root: {
root: { className: cn(
"font-sans text-base text-gray-600 bg-white p-3 border border-gray-300 transition-colors duration-200 appearance-none",
"hover:border-blue-500",
{
"rounded-lg": !props.showIcon,
"border-r-0 rounded-l-lg": props.showIcon,
}
),
},
}),
dropdownButton: {
root: ({ props }: CalendarPassThroughMethodOptions) => ({
className: cn({ "rounded-l-none": props.icon }),
}),
},
panel: ({ props }: CalendarPassThroughMethodOptions) => ({
className: cn("bg-[rgba(251,194,199,0.38)]", "min-w-full", {
"shadow-md border-0 absolute": !props.inline,
"inline-block overflow-x-auto border border-gray-300 p-2 rounded-lg": props.inline,
}),
}),
header: {
className: cn( className: cn(
"font-sans text-base text-gray-600 bg-white p-3 border border-gray-300 transition-colors duration-200 appearance-none", "flex items-center justify-between",
"hover:border-blue-500", "p-2 text-gray-700 bg-[rgba(251,194,199,0.38)] font-semibold m-0 border-b border-gray-300 rounded-t-lg"
{
"rounded-lg": !props.showIcon,
"border-r-0 rounded-l-lg": props.showIcon,
}
), ),
}, },
}), previousButton: {
dropdownButton: { className: cn(
root: ({ props }) => ({ "flex items-center justify-center cursor-pointer overflow-hidden relative",
className: cn({ "rounded-l-none": props.icon }), "w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out",
"hover:text-gray-700 hover:border-transparent hover:bg-gray-200 "
),
},
title: { className: "leading-8 mx-auto" },
monthTitle: {
className: cn("text-gray-700 transition duration-200 font-semibold p-2", "mr-2", "hover:text-blue-500"),
},
yearTitle: {
className: cn("text-gray-700 transition duration-200 font-semibold p-2", "hover:text-blue-500"),
},
nextButton: {
className: cn(
"flex items-center justify-center cursor-pointer overflow-hidden relative",
"w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out",
"hover:text-gray-700 hover:border-transparent hover:bg-gray-200 "
),
},
table: {
className: cn("border-collapse w-full", "my-2"),
},
tableHeaderCell: { className: "p-2" },
weekDay: { className: "text-gray-600 " },
day: { className: "p-2" },
dayLabel: ({ context }: CalendarPassThroughMethodOptions) => ({
className: cn(
"w-6 h-6 rounded-full transition-shadow duration-200 border-transparent border",
"flex items-center justify-center mx-auto overflow-hidden relative",
"focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] ",
{
"opacity-60 cursor-default": context.disabled,
"cursor-pointer": !context.disabled,
},
{
"text-gray-600 bg-transparent hover:bg-gray-200 ": !context.selected && !context.disabled,
"text-blue-700 bg-blue-100 hover:bg-blue-200": context.selected && !context.disabled,
}
),
}), }),
}, monthPicker: { className: "my-2" },
panel: ({ props }) => ({ month: ({ context }: CalendarPassThroughMethodOptions) => ({
className: cn("bg-[rgba(251,194,199,0.38)]", "min-w-full", { className: cn(
"shadow-md border-0 absolute": !props.inline, "w-1/3 inline-flex items-center justify-center cursor-pointer overflow-hidden relative",
"inline-block overflow-x-auto border border-gray-300 p-2 rounded-lg": props.inline, "p-2 transition-shadow duration-200 rounded-lg",
"focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] ",
{
"text-gray-600 bg-transparent hover:bg-gray-200 ": !context.selected && !context.disabled,
"text-blue-700 bg-blue-100 hover:bg-blue-200": context.selected && !context.disabled,
}
),
}), }),
}), yearPicker: {
header: { className: cn("my-2"),
className: cn( },
"flex items-center justify-between", year: ({ context }: CalendarPassThroughMethodOptions) => ({
"p-2 text-gray-700 bg-white font-semibold m-0 border-b border-gray-300 rounded-t-lg" className: cn(
), "w-1/2 inline-flex items-center justify-center cursor-pointer overflow-hidden relative",
}, "p-2 transition-shadow duration-200 rounded-lg",
previousButton: { "focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] ",
className: cn( {
"flex items-center justify-center cursor-pointer overflow-hidden relative", "text-gray-600 bg-transparent hover:bg-gray-200 ": !context.selected && !context.disabled,
"w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out", "text-blue-700 bg-blue-100 hover:bg-blue-200": context.selected && !context.disabled,
"hover:text-gray-700 hover:border-transparent hover:bg-gray-200 " }
), ),
}, }),
title: { className: "leading-8 mx-auto" }, timePicker: {
monthTitle: { className: cn("flex justify-center items-center", "border-t-1 border-solid border-gray-300"),
className: cn("text-gray-700 transition duration-200 font-semibold p-2", "mr-2", "hover:text-blue-500"), },
}, separatorContainer: { className: "flex items-center flex-col px-2" },
yearTitle: { separator: { className: "text-xl" },
className: cn("text-gray-700 transition duration-200 font-semibold p-2", "hover:text-blue-500"), hourPicker: { className: "flex items-center flex-col px-2" },
}, minutePicker: { className: "flex items-center flex-col px-2" },
nextButton: { ampmPicker: { className: "flex items-center flex-col px-2" },
className: cn( incrementButton: {
"flex items-center justify-center cursor-pointer overflow-hidden relative", className: cn(
"w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out", "flex items-center justify-center cursor-pointer overflow-hidden relative",
"hover:text-gray-700 hover:border-transparent hover:bg-gray-200 " "w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out",
), "hover:text-gray-700 hover:border-transparent hover:bg-gray-200 "
}, ),
table: { },
className: cn("border-collapse w-full", "my-2"), decrementButton: {
}, className: cn(
tableHeaderCell: { className: "p-2" }, "flex items-center justify-center cursor-pointer overflow-hidden relative",
weekDay: { className: "text-gray-600 " }, "w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out",
day: { className: "p-2" }, "hover:text-gray-700 hover:border-transparent hover:bg-gray-200 "
dayLabel: ({ context }) => ({ ),
className: cn( },
"w-10 h-10 rounded-full transition-shadow duration-200 border-transparent border", groupContainer: { className: "flex" },
"flex items-center justify-center mx-auto overflow-hidden relative", group: {
"focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] ", className: cn(
{ "flex-1",
"opacity-60 cursor-default": context.disabled, "border-l border-gray-300 pr-0.5 pl-0.5 pt-0 pb-0",
"cursor-pointer": !context.disabled, "first:pl-0 first:border-l-0"
}, ),
{ },
"text-gray-600 bg-transparent hover:bg-gray-200 ": !context.selected && !context.disabled, transition: TRANSITIONS.overlay,
"text-blue-700 bg-blue-100 hover:bg-blue-200": context.selected && !context.disabled, }}
} />
), <p class={cn("mt-2 text-center text-xl font-semibold", { hidden: !value })}>
}), {value && Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric" }).format(value)}
monthPicker: { className: "my-2" }, </p>
month: ({ context }) => ({ </div>
className: cn( <Button
"w-1/3 inline-flex items-center justify-center cursor-pointer overflow-hidden relative", className="flex w-full flex-row items-center justify-center gap-2 self-end"
"p-2 transition-shadow duration-200 rounded-lg", onClick={() => {
"focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] ", setShowTime(!showTime);
{
"text-gray-600 bg-transparent hover:bg-gray-200 ": !context.selected && !context.disabled,
"text-blue-700 bg-blue-100 hover:bg-blue-200": context.selected && !context.disabled,
}
),
}),
yearPicker: {
className: cn("my-2"),
},
year: ({ context }) => ({
className: cn(
"w-1/2 inline-flex items-center justify-center cursor-pointer overflow-hidden relative",
"p-2 transition-shadow duration-200 rounded-lg",
"focus:outline-none focus:outline-offset-0 focus:shadow-[0_0_0_0.2rem_rgba(191,219,254,1)] ",
{
"text-gray-600 bg-transparent hover:bg-gray-200 ": !context.selected && !context.disabled,
"text-blue-700 bg-blue-100 hover:bg-blue-200": context.selected && !context.disabled,
}
),
}),
timePicker: {
className: cn("flex justify-center items-center", "border-t-1 border-solid border-gray-300 p-2"),
},
separatorContainer: { className: "flex items-center flex-col px-2" },
separator: { className: "text-xl" },
hourPicker: { className: "flex items-center flex-col px-2" },
minutePicker: { className: "flex items-center flex-col px-2" },
ampmPicker: { className: "flex items-center flex-col px-2" },
incrementButton: {
className: cn(
"flex items-center justify-center cursor-pointer overflow-hidden relative",
"w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out",
"hover:text-gray-700 hover:border-transparent hover:bg-gray-200 "
),
},
decrementButton: {
className: cn(
"flex items-center justify-center cursor-pointer overflow-hidden relative",
"w-8 h-8 text-gray-600 border-0 bg-transparent rounded-full transition-colors duration-200 ease-in-out",
"hover:text-gray-700 hover:border-transparent hover:bg-gray-200 "
),
},
groupContainer: { className: "flex" },
group: {
className: cn("flex-1", "border-l border-gray-300 pr-0.5 pl-0.5 pt-0 pb-0", "first:pl-0 first:border-l-0"),
},
transition: TRANSITIONS.overlay,
}} }}
/> >
<ClockIcon class="h-8" /> <p>Время</p>
</Button>
</ModalWindow> </ModalWindow>
); );
}; };

View File

@@ -7,35 +7,43 @@ interface ModalWindowProps {
setIsOpen?: Dispatch<StateUpdater<boolean>>; setIsOpen?: Dispatch<StateUpdater<boolean>>;
onClose?: () => void; onClose?: () => void;
className?: string; className?: string;
zIndex?: number;
} }
const ModalWindow: FunctionComponent<ModalWindowProps> = ({ isOpen, children, setIsOpen, onClose, className = "" }) => { const ModalWindow: FunctionComponent<ModalWindowProps> = ({
isOpen,
children,
setIsOpen,
onClose,
className = "",
zIndex,
}) => {
useEffect(() => { useEffect(() => {
if (isOpen) return; if (isOpen) return;
if (onClose) onClose(); if (onClose) onClose();
}, [isOpen]); }, [isOpen]);
return ( return (
<div isOpen && (
onClick={(e) => {
e.stopPropagation();
if (setIsOpen) setIsOpen(false);
}}
class={cn(
"fixed top-0 left-0 z-50 flex h-screen w-screen cursor-pointer flex-col items-center justify-center bg-black/50",
{
hidden: !isOpen,
}
)}
>
<div <div
onClick={(e) => {
e.stopPropagation();
if (setIsOpen) setIsOpen(false);
}}
class={cn( class={cn(
"h-[40rem] w-[95%] cursor-auto rounded-[4rem] bg-white px-8 py-12 md:me-[20rem] md:h-[20rem] md:w-[65%] md:px-16", "fixed top-0 left-0 z-50 flex h-screen w-screen cursor-pointer flex-col items-center justify-center bg-black/50"
className
)} )}
onClick={(e) => e.stopPropagation()} style={{ zIndex: zIndex }}
> >
{children} <div
class={cn(
"flex h-[40rem] w-[95%] cursor-auto flex-col items-center justify-start rounded-[4rem] bg-white px-8 py-12 md:me-[20rem] md:h-[20rem] md:w-[65%] md:px-16",
className
)}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div> </div>
</div> )
); );
}; };

View File

@@ -17,6 +17,7 @@ import {
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { FunctionComponent } from "preact"; import { FunctionComponent } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks"; import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Nullable } from "primereact/ts-helpers";
import { ITask } from "./profile_tasks.dto"; import { ITask } from "./profile_tasks.dto";
import classes from "./profile_tasks.module.scss"; import classes from "./profile_tasks.module.scss";
@@ -64,14 +65,24 @@ const ProfileTasks: FunctionComponent = () => {
localStorage.setItem("tasks", JSON.stringify(tasks)); localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]); }, [tasks]);
const [openModal, setIsOpen] = useState(false); const [openModal, setIsOpen] = useState(false);
const [openModalCalendar, setOpenModalCalendar] = useState(false);
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [editContent, setEditContent] = useState<ITask | null>(null); const [editContent, setEditContent] = useState<ITask | null>(null);
const taskNameRef = useRef<HTMLInputElement>(null); const taskNameRef = useRef<HTMLInputElement>(null);
const taskDescriptionRef = useRef<HTMLTextAreaElement>(null); const taskDescriptionRef = useRef<HTMLTextAreaElement>(null);
const [calendarDate, setCalendarDate] = useState<Nullable<Date>>();
return ( return (
<div class={classes.container}> <div class={classes.container}>
<ModalCalendar isOpen /> <ModalCalendar
isOpen={openModalCalendar}
setIsOpen={setOpenModalCalendar}
onClose={() => {
if (isEdit) setCalendarDate(null);
}}
onChange={(e) => isCreating && setCalendarDate(e.value)}
value={calendarDate!}
/>
<ModalWindow <ModalWindow
isOpen={openModal} isOpen={openModal}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
@@ -79,6 +90,7 @@ const ProfileTasks: FunctionComponent = () => {
setIsEdit(false); setIsEdit(false);
setEditContent(null); setEditContent(null);
setIsCreating(false); setIsCreating(false);
setCalendarDate(null);
}} }}
> >
{isEdit && editContent && ( {isEdit && editContent && (
@@ -94,7 +106,13 @@ const ProfileTasks: FunctionComponent = () => {
</div> </div>
</div> </div>
<div class="flex flex-col items-center gap-6 self-center md:flex-row md:justify-start md:self-start"> <div class="flex flex-col items-center gap-6 self-center md:flex-row md:justify-start md:self-start">
<div class="flex h-full flex-row items-center gap-1 rounded-2xl bg-[rgba(251,194,199,0.38)] px-2 py-1"> <div
class="flex h-full flex-row items-center gap-1 rounded-2xl bg-[rgba(251,194,199,0.38)] px-2 py-1"
onClick={() => {
setOpenModalCalendar(true);
setCalendarDate(editContent.date);
}}
>
<CalendarDaysIcon class="size-10" /> <CalendarDaysIcon class="size-10" />
<p> <p>
{Intl.DateTimeFormat("ru-RU", { {Intl.DateTimeFormat("ru-RU", {
@@ -131,7 +149,13 @@ const ProfileTasks: FunctionComponent = () => {
ref={taskDescriptionRef} ref={taskDescriptionRef}
/> />
</div> </div>
<CalendarDaysIcon class="size-10 cursor-pointer" /> <CalendarDaysIcon
class="size-10 cursor-pointer"
onClick={() => {
setOpenModalCalendar(true);
setCalendarDate(calendarDate ?? new Date());
}}
/>
<BookmarkIcon class="ms-4 size-10 cursor-pointer" /> <BookmarkIcon class="ms-4 size-10 cursor-pointer" />
</div> </div>
<div className="mb-8 flex h-16 flex-col items-center gap-6 self-center md:mb-0 md:flex-row"> <div className="mb-8 flex h-16 flex-col items-center gap-6 self-center md:mb-0 md:flex-row">
@@ -151,16 +175,21 @@ const ProfileTasks: FunctionComponent = () => {
alert("Заполните все поля"); alert("Заполните все поля");
return; return;
} }
if (!calendarDate) {
alert("Заполните дату и время");
return;
}
const task: ITask = { const task: ITask = {
id: tasks.length + 1, id: tasks.length + 1,
name: taskNameRef.current.value, name: taskNameRef.current.value,
description: taskDescriptionRef.current.value, description: taskDescriptionRef.current.value,
date: new Date(), date: calendarDate,
checked: false, checked: false,
tags: ["Математика", "Домашнее задание"], tags: ["Математика", "Домашнее задание"],
}; };
setTasks([...tasks, task]); setTasks([...tasks, task]);
setIsOpen(false); setIsOpen(false);
setCalendarDate(null);
} }
}} }}
> >
@@ -175,7 +204,7 @@ const ProfileTasks: FunctionComponent = () => {
<div class={classes.header}>Сегодня: {getDate}</div> <div class={classes.header}>Сегодня: {getDate}</div>
<div class={classes.tasks_container}> <div class={classes.tasks_container}>
{tasks {tasks
.sort((a, b) => b.id - a.id) .sort((a, b) => b.date.getTime() - a.date.getTime())
.map((task) => ( .map((task) => (
<Task <Task
name={task.name} name={task.name}