4 Commits

Author SHA1 Message Date
c629f0dcf8 feat: form hook 2025-04-23 12:32:27 +03:00
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
10 changed files with 289 additions and 195 deletions

View File

@@ -14,6 +14,7 @@
"preact-iso": "^2.9.1",
"primelocale": "^2.1.2",
"primereact": "^10.9.4",
"react-hook-form": "^7.56.1",
"tailwind-merge": "^3.0.2",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.17",
@@ -612,6 +613,8 @@
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
"react-hook-form": ["react-hook-form@7.56.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ=="],
"react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="],

View File

@@ -19,6 +19,7 @@
"preact-iso": "^2.9.1",
"primelocale": "^2.1.2",
"primereact": "^10.9.4",
"react-hook-form": "^7.56.1",
"tailwind-merge": "^3.0.2",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.17"

View File

@@ -1,13 +1,18 @@
import { cn } from "@/utils/class-merge";
import { ClockIcon } from "@heroicons/react/24/outline";
import { FunctionComponent } from "preact";
import { Dispatch, StateUpdater } from "preact/hooks";
import { Calendar } from "primereact/calendar";
import { Dispatch, StateUpdater, useState } from "preact/hooks";
import { Calendar, CalendarPassThroughMethodOptions } from "primereact/calendar";
import { FormEvent } from "primereact/ts-helpers";
import Button from "./ui/Button";
import ModalWindow from "./ui/Modal";
interface ModalCalendarProps {
isOpen?: boolean;
setIsOpen?: Dispatch<StateUpdater<boolean>>;
onClose?: () => void;
value?: Date;
onChange?: (e: FormEvent<Date>) => void;
}
const TRANSITIONS = {
@@ -22,18 +27,33 @@ const TRANSITIONS = {
},
};
const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpen, onClose }) => {
const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpen, onClose, onChange, value }) => {
const [showTime, setShowTime] = useState(false);
return (
<ModalWindow isOpen={isOpen} setIsOpen={setIsOpen} onClose={onClose} className="md:h-[40rem] md:w-[30rem]">
<ModalWindow
isOpen={isOpen}
setIsOpen={setIsOpen}
onClose={() => {
onClose!();
setShowTime(false);
}}
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 }) => ({
className: cn("inline-flex max-w-full relative", {
root: ({ props }: CalendarPassThroughMethodOptions) => ({
className: cn("inline-flex w-full relative", {
"opacity-60 select-none pointer-events-none cursor-default": props.disabled,
}),
}),
input: ({ props }) => ({
input: ({ props }: CalendarPassThroughMethodOptions) => ({
root: {
className: cn(
"font-sans text-base text-gray-600 bg-white p-3 border border-gray-300 transition-colors duration-200 appearance-none",
@@ -46,11 +66,11 @@ const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpe
},
}),
dropdownButton: {
root: ({ props }) => ({
root: ({ props }: CalendarPassThroughMethodOptions) => ({
className: cn({ "rounded-l-none": props.icon }),
}),
},
panel: ({ props }) => ({
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,
@@ -59,7 +79,7 @@ const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpe
header: {
className: cn(
"flex items-center justify-between",
"p-2 text-gray-700 bg-white font-semibold m-0 border-b border-gray-300 rounded-t-lg"
"p-2 text-gray-700 bg-[rgba(251,194,199,0.38)] font-semibold m-0 border-b border-gray-300 rounded-t-lg"
),
},
previousButton: {
@@ -89,9 +109,9 @@ const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpe
tableHeaderCell: { className: "p-2" },
weekDay: { className: "text-gray-600 " },
day: { className: "p-2" },
dayLabel: ({ context }) => ({
dayLabel: ({ context }: CalendarPassThroughMethodOptions) => ({
className: cn(
"w-10 h-10 rounded-full transition-shadow duration-200 border-transparent border",
"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)] ",
{
@@ -105,7 +125,7 @@ const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpe
),
}),
monthPicker: { className: "my-2" },
month: ({ context }) => ({
month: ({ context }: CalendarPassThroughMethodOptions) => ({
className: cn(
"w-1/3 inline-flex items-center justify-center cursor-pointer overflow-hidden relative",
"p-2 transition-shadow duration-200 rounded-lg",
@@ -119,7 +139,7 @@ const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpe
yearPicker: {
className: cn("my-2"),
},
year: ({ context }) => ({
year: ({ context }: CalendarPassThroughMethodOptions) => ({
className: cn(
"w-1/2 inline-flex items-center justify-center cursor-pointer overflow-hidden relative",
"p-2 transition-shadow duration-200 rounded-lg",
@@ -131,7 +151,7 @@ const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpe
),
}),
timePicker: {
className: cn("flex justify-center items-center", "border-t-1 border-solid border-gray-300 p-2"),
className: cn("flex justify-center items-center", "border-t-1 border-solid border-gray-300"),
},
separatorContainer: { className: "flex items-center flex-col px-2" },
separator: { className: "text-xl" },
@@ -154,11 +174,27 @@ const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpe
},
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"),
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,
}}
/>
<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)}
</p>
</div>
<Button
className="flex w-full flex-row items-center justify-center gap-2 self-end"
onClick={() => {
setShowTime(!showTime);
}}
>
<ClockIcon class="h-8" /> <p>Время</p>
</Button>
</ModalWindow>
);
};

View File

@@ -1,5 +1,5 @@
@reference '../../index.scss';
.button {
@apply rounded-4xl px-4 py-3 text-xl text-black shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-colors hover:cursor-pointer;
@apply rounded-4xl px-4 py-3 text-xl text-black shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-colors hover:cursor-pointer focus:outline-1;
}

View File

@@ -16,6 +16,7 @@ interface ButtonProps {
color?: "primary" | "secondary" | "red";
onClick?: () => void;
className?: string;
type?: "button" | "submit";
}
const Button: FunctionComponent<ButtonProps> = ({
@@ -23,9 +24,10 @@ const Button: FunctionComponent<ButtonProps> = ({
onClick = () => {},
color = "primary",
className = "",
type = "button",
}) => {
return (
<button type="button" class={button({ color: color, class: className })} onClick={onClick}>
<button type={type} class={button({ color: color, class: className })} onClick={onClick}>
{children}
</button>
);

View File

@@ -1,5 +1,5 @@
import { cn } from "@/utils/class-merge";
import { FunctionComponent, Ref } from "preact";
import { forwardRef, HTMLProps, useEffect } from "preact/compat";
import { tv } from "tailwind-variants";
import classes from "./Input.module.scss";
@@ -20,29 +20,19 @@ const input = tv({
},
});
interface InputProps {
isPassword?: boolean;
placeholder?: string;
interface InputProps extends HTMLProps<HTMLInputElement> {
textAlign?: "center" | "left";
error?: string;
textRef?: Ref<HTMLInputElement> | null;
}
const Input: FunctionComponent<InputProps> = ({
isPassword = false,
placeholder = "",
textAlign,
error = "",
textRef = null,
}: InputProps) => {
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const { textAlign, error, type = "text", ...rest } = props;
useEffect(() => {
console.log(`error: ${error}`);
}, [error]);
return (
<div class="flex w-full flex-col items-center gap-1">
<input
type={isPassword ? "password" : "text"}
class={input({ "text-align": textAlign, "border-error": error !== "" })}
placeholder={placeholder}
ref={textRef}
/>
<input class={input({ "text-align": textAlign, "border-error": !!error })} ref={ref} type={type} {...rest} />
<p
class={cn("invisible h-10 w-[80%] text-center text-[0.7rem] break-words text-red-500", {
visible: error !== "",
@@ -52,6 +42,8 @@ const Input: FunctionComponent<InputProps> = ({
</p>
</div>
);
};
});
Input.displayName = "AHInput";
export default Input;

View File

@@ -7,28 +7,35 @@ interface ModalWindowProps {
setIsOpen?: Dispatch<StateUpdater<boolean>>;
onClose?: () => void;
className?: string;
zIndex?: number;
}
const ModalWindow: FunctionComponent<ModalWindowProps> = ({ isOpen, children, setIsOpen, onClose, className = "" }) => {
const ModalWindow: FunctionComponent<ModalWindowProps> = ({
isOpen,
children,
setIsOpen,
onClose,
className = "",
zIndex,
}) => {
useEffect(() => {
if (isOpen) return;
if (onClose) onClose();
}, [isOpen]);
return (
isOpen && (
<div
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,
}
"fixed top-0 left-0 z-50 flex h-screen w-screen cursor-pointer flex-col items-center justify-center bg-black/50"
)}
style={{ zIndex: zIndex }}
>
<div
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",
"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()}
@@ -36,6 +43,7 @@ const ModalWindow: FunctionComponent<ModalWindowProps> = ({ isOpen, children, se
{children}
</div>
</div>
)
);
};

4
src/pages/login.dto.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface ILoginForm {
login: string;
password: string;
}

View File

@@ -6,7 +6,9 @@ import { useAppContext } from "@/providers/AuthProvider";
import { FunctionComponent } from "preact";
import { useLocation } from "preact-iso";
import "preact/debug";
import { useRef, useState } from "preact/hooks";
import { useState } from "preact/hooks";
import { Controller, SubmitHandler, useForm } from "react-hook-form";
import { ILoginForm } from "./login.dto";
import classes from "./login.module.scss";
const testUser = {
@@ -16,37 +18,54 @@ const testUser = {
const LoginPage: FunctionComponent = () => {
const { isLoggedIn } = useAppContext();
const { route } = useLocation();
const loginRef = useRef<HTMLInputElement | null>(null);
const passwordRef = useRef<HTMLInputElement | null>(null);
const [loginError, setLoginError] = useState("");
const [passwordError, setPasswordError] = useState("");
const login = async () => {
if (!loginRef.current || !passwordRef.current) return;
setLoginError("");
setPasswordError("");
if (!loginRef.current.value.length || !passwordRef.current.value.length) {
if (!loginRef.current.value.length) setLoginError("Введите логин");
if (!passwordRef.current.value.length) setPasswordError("Введите пароль");
return;
}
if (loginRef.current.value !== testUser.login || passwordRef.current.value !== testUser.password) {
setLoginError("Неправильный логин или пароль");
const login: SubmitHandler<ILoginForm> = async (data) => {
console.log(data);
if (data.login !== testUser.login || data.password !== testUser.password) {
setError("login", { message: "Неверный" }); //TODO: не показывает ошибку
return;
}
isLoggedIn.value = true;
localStorage.setItem("loggedIn", "true");
route("/profile/tasks", true);
};
const { control, handleSubmit, formState, setError } = useForm({
defaultValues: {
login: "",
password: "",
},
mode: "onChange",
});
if (isLoggedIn.value) route("/profile/tasks", true);
return !isLoggedIn.value ? (
<div class={classes.login_container}>
<div class={classes.login_card}>
<p class={classes.login_card_name}>Антихвост</p>
<Input placeholder="Логин" textAlign="center" textRef={loginRef} error={loginError} />
<Input isPassword placeholder="Пароль" textAlign="center" textRef={passwordRef} error={passwordError} />
<Button color="secondary" onClick={login}>
<form onSubmit={handleSubmit((data) => login(data))}>
<Controller
name="login"
control={control}
rules={{
required: "Введите логин",
}}
render={({ field }) => (
<Input placeholder="Логин" textAlign="center" error={formState.errors.login?.message} {...field} />
)}
/>
<Controller
name="password"
control={control}
rules={{
required: "Введите пароль",
}}
render={({ field }) => (
<Input placeholder="Пароль" textAlign="center" type="password" error={passwordError} {...field} />
)}
/>
<Button type="submit" color="secondary" className="w-full">
Войти
</Button>
</form>
</div>
</div>
) : (

View File

@@ -17,6 +17,7 @@ import {
} from "@heroicons/react/24/outline";
import { FunctionComponent } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import { Nullable } from "primereact/ts-helpers";
import { ITask } from "./profile_tasks.dto";
import classes from "./profile_tasks.module.scss";
@@ -64,14 +65,24 @@ const ProfileTasks: FunctionComponent = () => {
localStorage.setItem("tasks", JSON.stringify(tasks));
}, [tasks]);
const [openModal, setIsOpen] = useState(false);
const [openModalCalendar, setOpenModalCalendar] = useState(false);
const [isEdit, setIsEdit] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [editContent, setEditContent] = useState<ITask | null>(null);
const taskNameRef = useRef<HTMLInputElement>(null);
const taskDescriptionRef = useRef<HTMLTextAreaElement>(null);
const [calendarDate, setCalendarDate] = useState<Nullable<Date>>();
return (
<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
isOpen={openModal}
setIsOpen={setIsOpen}
@@ -79,6 +90,7 @@ const ProfileTasks: FunctionComponent = () => {
setIsEdit(false);
setEditContent(null);
setIsCreating(false);
setCalendarDate(null);
}}
>
{isEdit && editContent && (
@@ -94,7 +106,13 @@ const ProfileTasks: FunctionComponent = () => {
</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 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" />
<p>
{Intl.DateTimeFormat("ru-RU", {
@@ -131,7 +149,13 @@ const ProfileTasks: FunctionComponent = () => {
ref={taskDescriptionRef}
/>
</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" />
</div>
<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("Заполните все поля");
return;
}
if (!calendarDate) {
alert("Заполните дату и время");
return;
}
const task: ITask = {
id: tasks.length + 1,
name: taskNameRef.current.value,
description: taskDescriptionRef.current.value,
date: new Date(),
date: calendarDate,
checked: false,
tags: ["Математика", "Домашнее задание"],
};
setTasks([...tasks, task]);
setIsOpen(false);
setCalendarDate(null);
}
}}
>
@@ -175,7 +204,7 @@ const ProfileTasks: FunctionComponent = () => {
<div class={classes.header}>Сегодня: {getDate}</div>
<div class={classes.tasks_container}>
{tasks
.sort((a, b) => b.id - a.id)
.sort((a, b) => b.date.getTime() - a.date.getTime())
.map((task) => (
<Task
name={task.name}