Compare commits
9 Commits
a99be691c7
...
form
| Author | SHA1 | Date | |
|---|---|---|---|
| c629f0dcf8 | |||
| 2a7d41bba5 | |||
| d7c406a930 | |||
| 7a98dbfe91 | |||
| 607637b5ca | |||
| 66d7cf0532 | |||
| ce55ca5bfe | |||
| 174ea24f4e | |||
| c4eca9b5e6 |
3
bun.lock
3
bun.lock
@@ -14,6 +14,7 @@
|
|||||||
"preact-iso": "^2.9.1",
|
"preact-iso": "^2.9.1",
|
||||||
"primelocale": "^2.1.2",
|
"primelocale": "^2.1.2",
|
||||||
"primereact": "^10.9.4",
|
"primereact": "^10.9.4",
|
||||||
|
"react-hook-form": "^7.56.1",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwind-variants": "^1.0.0",
|
"tailwind-variants": "^1.0.0",
|
||||||
"tailwindcss": "^4.0.17",
|
"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-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-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=="],
|
"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=="],
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"preact-iso": "^2.9.1",
|
"preact-iso": "^2.9.1",
|
||||||
"primelocale": "^2.1.2",
|
"primelocale": "^2.1.2",
|
||||||
"primereact": "^10.9.4",
|
"primereact": "^10.9.4",
|
||||||
|
"react-hook-form": "^7.56.1",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwind-variants": "^1.0.0",
|
"tailwind-variants": "^1.0.0",
|
||||||
"tailwindcss": "^4.0.17"
|
"tailwindcss": "^4.0.17"
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ import "preact/debug";
|
|||||||
import { ru } from "primelocale/js/ru.js";
|
import { ru } from "primelocale/js/ru.js";
|
||||||
import { addLocale, locale, PrimeReactProvider } from "primereact/api";
|
import { addLocale, locale, PrimeReactProvider } from "primereact/api";
|
||||||
import { useMountEffect } from "primereact/hooks";
|
import { useMountEffect } from "primereact/hooks";
|
||||||
import Tailwind from "primereact/passthrough/tailwind";
|
|
||||||
import Page404 from "./pages/404";
|
import Page404 from "./pages/404";
|
||||||
import LoginPage from "./pages/login";
|
import LoginPage from "./pages/login";
|
||||||
import { AppProvider, useAppContext } from "./providers/AuthProvider";
|
import { AppProvider, useAppContext } from "./providers/AuthProvider";
|
||||||
import { cn } from "./utils/class-merge";
|
|
||||||
|
|
||||||
const HomePage: FunctionComponent = () => {
|
const HomePage: FunctionComponent = () => {
|
||||||
const { route } = useLocation();
|
const { route } = useLocation();
|
||||||
@@ -28,8 +26,8 @@ export function App() {
|
|||||||
<PrimeReactProvider
|
<PrimeReactProvider
|
||||||
value={{
|
value={{
|
||||||
unstyled: true,
|
unstyled: true,
|
||||||
pt: Tailwind,
|
// pt: Tailwind,
|
||||||
ptOptions: { mergeProps: true, mergeSections: true, classNameMergeFunction: cn },
|
// ptOptions: { mergeProps: true, mergeSections: true, classNameMergeFunction: cn },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LocationProvider>
|
<LocationProvider>
|
||||||
|
|||||||
202
src/components/ModalCalendar.tsx
Normal file
202
src/components/ModalCalendar.tsx
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import { cn } from "@/utils/class-merge";
|
||||||
|
import { ClockIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { FunctionComponent } from "preact";
|
||||||
|
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 = {
|
||||||
|
overlay: {
|
||||||
|
timeout: 150,
|
||||||
|
cn: {
|
||||||
|
enter: "opacity-0 scale-75",
|
||||||
|
enterActive: "opacity-100 !scale-100 transition-[transform,opacity] duration-150 ease-in",
|
||||||
|
exit: "opacity-100",
|
||||||
|
exitActive: "!opacity-0 transition-opacity duration-150 ease-linear",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModalCalendar: FunctionComponent<ModalCalendarProps> = ({ isOpen, setIsOpen, onClose, onChange, value }) => {
|
||||||
|
const [showTime, setShowTime] = useState(false);
|
||||||
|
return (
|
||||||
|
<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 }: CalendarPassThroughMethodOptions) => ({
|
||||||
|
className: cn("inline-flex w-full relative", {
|
||||||
|
"opacity-60 select-none pointer-events-none cursor-default": props.disabled,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
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",
|
||||||
|
"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(
|
||||||
|
"flex items-center justify-between",
|
||||||
|
"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: {
|
||||||
|
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 "
|
||||||
|
),
|
||||||
|
},
|
||||||
|
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" },
|
||||||
|
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",
|
||||||
|
"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: {
|
||||||
|
className: cn("my-2"),
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
"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"),
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalCalendar;
|
||||||
@@ -5,6 +5,7 @@ import classes from "./task.module.scss";
|
|||||||
interface TaskProps {
|
interface TaskProps {
|
||||||
name: string;
|
name: string;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskStyle = tv({
|
const taskStyle = tv({
|
||||||
@@ -27,10 +28,10 @@ const markStyle = tv({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const Task: FunctionComponent<TaskProps> = ({ name, checked = false }: TaskProps) => {
|
const Task: FunctionComponent<TaskProps> = ({ name, checked = false, onClick = () => {} }: TaskProps) => {
|
||||||
return (
|
return (
|
||||||
// Временное действие для тестирования
|
// Временное действие для тестирования
|
||||||
<button onClick={() => alert(name)} class="w-[95%]">
|
<button onClick={onClick} class="w-[95%]">
|
||||||
<div class={classes.task}>
|
<div class={classes.task}>
|
||||||
<div class={taskStyle({ checked })}>
|
<div class={taskStyle({ checked })}>
|
||||||
<p class={markStyle({ checked })}>✓</p>
|
<p class={markStyle({ checked })}>✓</p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@reference '../../index.scss';
|
@reference '../../index.scss';
|
||||||
|
|
||||||
.button {
|
.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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,27 @@ const button = tv({
|
|||||||
color: {
|
color: {
|
||||||
primary: "bg-[rgba(206,232,251,0.7)] hover:bg-[rgba(206,232,251,0.9)] active:bg-[rgba(206,232,251,0.9)]",
|
primary: "bg-[rgba(206,232,251,0.7)] hover:bg-[rgba(206,232,251,0.9)] active:bg-[rgba(206,232,251,0.9)]",
|
||||||
secondary: "bg-[rgba(255,251,197,0.68)] hover:bg-[rgba(255,251,197,0.9)] active:bg-[rgba(255,251,197,0.9)]",
|
secondary: "bg-[rgba(255,251,197,0.68)] hover:bg-[rgba(255,251,197,0.9)] active:bg-[rgba(255,251,197,0.9)]",
|
||||||
|
red: "bg-[rgba(251,194,199,0.53)] hover:bg-[rgba(251,194,199,0.9)] active:bg-[rgba(251,194,199,0.9)]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
color?: "primary" | "secondary";
|
color?: "primary" | "secondary" | "red";
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
className?: string;
|
||||||
|
type?: "button" | "submit";
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button: FunctionComponent<ButtonProps> = ({ children, onClick = () => {}, color = "primary" }) => {
|
const Button: FunctionComponent<ButtonProps> = ({
|
||||||
|
children,
|
||||||
|
onClick = () => {},
|
||||||
|
color = "primary",
|
||||||
|
className = "",
|
||||||
|
type = "button",
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<button type="button" class={button({ color: color })} onClick={onClick}>
|
<button type={type} class={button({ color: color, class: className })} onClick={onClick}>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { cn } from "@/utils/class-merge";
|
import { cn } from "@/utils/class-merge";
|
||||||
import { FunctionComponent, Ref } from "preact";
|
import { forwardRef, HTMLProps, useEffect } from "preact/compat";
|
||||||
import { tv } from "tailwind-variants";
|
import { tv } from "tailwind-variants";
|
||||||
import classes from "./Input.module.scss";
|
import classes from "./Input.module.scss";
|
||||||
|
|
||||||
@@ -20,29 +20,19 @@ const input = tv({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface InputProps {
|
interface InputProps extends HTMLProps<HTMLInputElement> {
|
||||||
isPassword?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
textAlign?: "center" | "left";
|
textAlign?: "center" | "left";
|
||||||
error?: string;
|
error?: string;
|
||||||
textRef?: Ref<HTMLInputElement> | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Input: FunctionComponent<InputProps> = ({
|
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||||
isPassword = false,
|
const { textAlign, error, type = "text", ...rest } = props;
|
||||||
placeholder = "",
|
useEffect(() => {
|
||||||
textAlign,
|
console.log(`error: ${error}`);
|
||||||
error = "",
|
}, [error]);
|
||||||
textRef = null,
|
|
||||||
}: InputProps) => {
|
|
||||||
return (
|
return (
|
||||||
<div class="flex w-full flex-col items-center gap-1">
|
<div class="flex w-full flex-col items-center gap-1">
|
||||||
<input
|
<input class={input({ "text-align": textAlign, "border-error": !!error })} ref={ref} type={type} {...rest} />
|
||||||
type={isPassword ? "password" : "text"}
|
|
||||||
class={input({ "text-align": textAlign, "border-error": error !== "" })}
|
|
||||||
placeholder={placeholder}
|
|
||||||
ref={textRef}
|
|
||||||
/>
|
|
||||||
<p
|
<p
|
||||||
class={cn("invisible h-10 w-[80%] text-center text-[0.7rem] break-words text-red-500", {
|
class={cn("invisible h-10 w-[80%] text-center text-[0.7rem] break-words text-red-500", {
|
||||||
visible: error !== "",
|
visible: error !== "",
|
||||||
@@ -52,6 +42,8 @@ const Input: FunctionComponent<InputProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
Input.displayName = "AHInput";
|
||||||
|
|
||||||
export default Input;
|
export default Input;
|
||||||
|
|||||||
50
src/components/ui/Modal.tsx
Normal file
50
src/components/ui/Modal.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { cn } from "@/utils/class-merge";
|
||||||
|
import { FunctionComponent } from "preact";
|
||||||
|
import { Dispatch, StateUpdater, useEffect } from "preact/hooks";
|
||||||
|
|
||||||
|
interface ModalWindowProps {
|
||||||
|
isOpen?: boolean;
|
||||||
|
setIsOpen?: Dispatch<StateUpdater<boolean>>;
|
||||||
|
onClose?: () => void;
|
||||||
|
className?: string;
|
||||||
|
zIndex?: number;
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
)}
|
||||||
|
style={{ zIndex: zIndex }}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalWindow;
|
||||||
4
src/pages/login.dto.ts
Normal file
4
src/pages/login.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ILoginForm {
|
||||||
|
login: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
@@ -6,7 +6,9 @@ import { useAppContext } from "@/providers/AuthProvider";
|
|||||||
import { FunctionComponent } from "preact";
|
import { FunctionComponent } from "preact";
|
||||||
import { useLocation } from "preact-iso";
|
import { useLocation } from "preact-iso";
|
||||||
import "preact/debug";
|
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";
|
import classes from "./login.module.scss";
|
||||||
|
|
||||||
const testUser = {
|
const testUser = {
|
||||||
@@ -16,37 +18,54 @@ const testUser = {
|
|||||||
const LoginPage: FunctionComponent = () => {
|
const LoginPage: FunctionComponent = () => {
|
||||||
const { isLoggedIn } = useAppContext();
|
const { isLoggedIn } = useAppContext();
|
||||||
const { route } = useLocation();
|
const { route } = useLocation();
|
||||||
const loginRef = useRef<HTMLInputElement | null>(null);
|
|
||||||
const passwordRef = useRef<HTMLInputElement | null>(null);
|
|
||||||
const [loginError, setLoginError] = useState("");
|
|
||||||
const [passwordError, setPasswordError] = useState("");
|
const [passwordError, setPasswordError] = useState("");
|
||||||
const login = async () => {
|
const login: SubmitHandler<ILoginForm> = async (data) => {
|
||||||
if (!loginRef.current || !passwordRef.current) return;
|
console.log(data);
|
||||||
setLoginError("");
|
if (data.login !== testUser.login || data.password !== testUser.password) {
|
||||||
setPasswordError("");
|
setError("login", { message: "Неверный" }); //TODO: не показывает ошибку
|
||||||
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("Неправильный логин или пароль");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isLoggedIn.value = true;
|
isLoggedIn.value = true;
|
||||||
localStorage.setItem("loggedIn", "true");
|
localStorage.setItem("loggedIn", "true");
|
||||||
route("/profile/tasks", true);
|
route("/profile/tasks", true);
|
||||||
};
|
};
|
||||||
|
const { control, handleSubmit, formState, setError } = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
login: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
if (isLoggedIn.value) route("/profile/tasks", true);
|
if (isLoggedIn.value) route("/profile/tasks", true);
|
||||||
return !isLoggedIn.value ? (
|
return !isLoggedIn.value ? (
|
||||||
<div class={classes.login_container}>
|
<div class={classes.login_container}>
|
||||||
<div class={classes.login_card}>
|
<div class={classes.login_card}>
|
||||||
<p class={classes.login_card_name}>Антихвост</p>
|
<p class={classes.login_card_name}>Антихвост</p>
|
||||||
<Input placeholder="Логин" textAlign="center" textRef={loginRef} error={loginError} />
|
<form onSubmit={handleSubmit((data) => login(data))}>
|
||||||
<Input isPassword placeholder="Пароль" textAlign="center" textRef={passwordRef} error={passwordError} />
|
<Controller
|
||||||
<Button color="secondary" onClick={login}>
|
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>
|
</Button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
8
src/pages/profile_tasks.dto.ts
Normal file
8
src/pages/profile_tasks.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface ITask {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
checked: boolean;
|
||||||
|
date: Date;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
@@ -1,13 +1,52 @@
|
|||||||
import Task from "@/components/task";
|
import Task from "@/components/task";
|
||||||
|
|
||||||
|
import ModalCalendar from "@/components/ModalCalendar";
|
||||||
|
import Button from "@/components/ui/Button";
|
||||||
|
import ModalWindow from "@/components/ui/Modal";
|
||||||
import { withTitle } from "@/constructors/Component";
|
import { withTitle } from "@/constructors/Component";
|
||||||
import { UrlsTitle } from "@/enums/urls";
|
import { UrlsTitle } from "@/enums/urls";
|
||||||
import { PlusIcon } from "@heroicons/react/20/solid";
|
import { PlusIcon } from "@heroicons/react/20/solid";
|
||||||
import { FunnelIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
import {
|
||||||
|
BookmarkIcon,
|
||||||
|
BookOpenIcon,
|
||||||
|
CalendarDaysIcon,
|
||||||
|
DocumentDuplicateIcon,
|
||||||
|
FunnelIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
PencilIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
import { FunctionComponent } from "preact";
|
import { FunctionComponent } from "preact";
|
||||||
import { useMemo } from "preact/hooks";
|
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";
|
import classes from "./profile_tasks.module.scss";
|
||||||
|
|
||||||
const example_tasks = ["Test 1", "Test 2", "Test 3", "Test 4", "Test 5", "Test 6", "Test 7", "Test 8"];
|
const example_tasks: ITask[] = [
|
||||||
|
{
|
||||||
|
checked: false,
|
||||||
|
date: new Date(),
|
||||||
|
description: "test",
|
||||||
|
id: 1,
|
||||||
|
name: "test1",
|
||||||
|
tags: ["Программирование", "Лабораторная работа"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checked: true,
|
||||||
|
date: new Date(2025, 6, 2),
|
||||||
|
description: "test2",
|
||||||
|
id: 3,
|
||||||
|
name: "test3",
|
||||||
|
tags: ["Информатика", "Практическая работа"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
checked: false,
|
||||||
|
date: new Date(2025, 5, 1),
|
||||||
|
description: "test3",
|
||||||
|
id: 2,
|
||||||
|
name: "test2",
|
||||||
|
tags: ["Математика", "Домашнее задание"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const ProfileTasks: FunctionComponent = () => {
|
const ProfileTasks: FunctionComponent = () => {
|
||||||
const getDate = useMemo(() => {
|
const getDate = useMemo(() => {
|
||||||
@@ -15,18 +54,178 @@ const ProfileTasks: FunctionComponent = () => {
|
|||||||
const formatter = new Intl.DateTimeFormat("ru-RU", { month: "long", day: "numeric" });
|
const formatter = new Intl.DateTimeFormat("ru-RU", { month: "long", day: "numeric" });
|
||||||
return formatter.format(date);
|
return formatter.format(date);
|
||||||
}, []);
|
}, []);
|
||||||
|
const init_tasks: ITask[] = localStorage.getItem("tasks")
|
||||||
|
? JSON.parse(localStorage.getItem("tasks") as string)
|
||||||
|
: example_tasks;
|
||||||
|
init_tasks.forEach((task) => {
|
||||||
|
task.date = new Date(task.date);
|
||||||
|
});
|
||||||
|
const [tasks, setTasks] = useState<ITask[]>(init_tasks);
|
||||||
|
useEffect(() => {
|
||||||
|
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 (
|
return (
|
||||||
<div class={classes.container}>
|
<div class={classes.container}>
|
||||||
{example_tasks.length > 0 ? (
|
<ModalCalendar
|
||||||
|
isOpen={openModalCalendar}
|
||||||
|
setIsOpen={setOpenModalCalendar}
|
||||||
|
onClose={() => {
|
||||||
|
if (isEdit) setCalendarDate(null);
|
||||||
|
}}
|
||||||
|
onChange={(e) => isCreating && setCalendarDate(e.value)}
|
||||||
|
value={calendarDate!}
|
||||||
|
/>
|
||||||
|
<ModalWindow
|
||||||
|
isOpen={openModal}
|
||||||
|
setIsOpen={setIsOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsEdit(false);
|
||||||
|
setEditContent(null);
|
||||||
|
setIsCreating(false);
|
||||||
|
setCalendarDate(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isEdit && editContent && (
|
||||||
|
<div class="flex h-full w-full flex-col items-start justify-between">
|
||||||
|
<div class="flex w-full flex-row items-start justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<p class="text-2xl">{editContent.name}</p>
|
||||||
|
<p>{editContent.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex cursor-pointer flex-col items-center gap-3">
|
||||||
|
<PencilIcon class="size-6" />
|
||||||
|
<p class="text-[0.7rem]">Редактировать</p>
|
||||||
|
</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"
|
||||||
|
onClick={() => {
|
||||||
|
setOpenModalCalendar(true);
|
||||||
|
setCalendarDate(editContent.date);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CalendarDaysIcon class="size-10" />
|
||||||
|
<p>
|
||||||
|
{Intl.DateTimeFormat("ru-RU", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}).format(editContent.date)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex h-full flex-col items-start gap-1 rounded-2xl bg-[rgba(251,194,199,0.38)] px-4 py-2">
|
||||||
|
<p class="flex flex-row gap-2">
|
||||||
|
<BookOpenIcon class="size-5" />
|
||||||
|
{editContent.tags[0]}
|
||||||
|
</p>
|
||||||
|
<p class="flex flex-row gap-2">
|
||||||
|
<DocumentDuplicateIcon class="size-5" />
|
||||||
|
{editContent.tags[1]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isCreating && (
|
||||||
|
<div class="flex h-full w-full flex-col items-start justify-between">
|
||||||
|
<div class="flex w-full flex-row items-start justify-between">
|
||||||
|
<div class="me-4 flex flex-1 flex-col gap-1">
|
||||||
|
<input class="text-2xl outline-0" maxLength={20} placeholder="Название" ref={taskNameRef} />
|
||||||
|
<textarea
|
||||||
|
class="h-[5rem] w-full resize-none outline-0"
|
||||||
|
maxLength={200}
|
||||||
|
placeholder="Описание"
|
||||||
|
ref={taskDescriptionRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<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">
|
||||||
|
<Button
|
||||||
|
className="text-sm"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="red"
|
||||||
|
onClick={() => {
|
||||||
|
if (taskNameRef.current && taskDescriptionRef.current) {
|
||||||
|
if (!taskNameRef.current.value || !taskDescriptionRef.current.value) {
|
||||||
|
alert("Заполните все поля");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!calendarDate) {
|
||||||
|
alert("Заполните дату и время");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const task: ITask = {
|
||||||
|
id: tasks.length + 1,
|
||||||
|
name: taskNameRef.current.value,
|
||||||
|
description: taskDescriptionRef.current.value,
|
||||||
|
date: calendarDate,
|
||||||
|
checked: false,
|
||||||
|
tags: ["Математика", "Домашнее задание"],
|
||||||
|
};
|
||||||
|
setTasks([...tasks, task]);
|
||||||
|
setIsOpen(false);
|
||||||
|
setCalendarDate(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Добавить задачу
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalWindow>
|
||||||
|
{tasks.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div class={classes.header}>Сегодня: {getDate}</div>
|
<div class={classes.header}>Сегодня: {getDate}</div>
|
||||||
<div class={classes.tasks_container}>
|
<div class={classes.tasks_container}>
|
||||||
{example_tasks.map((task, index) => (
|
{tasks
|
||||||
<Task name={task} key={index} checked={index % 2 === 0} />
|
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
||||||
|
.map((task) => (
|
||||||
|
<Task
|
||||||
|
name={task.name}
|
||||||
|
key={task.id}
|
||||||
|
checked={task.checked}
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
setIsEdit(true);
|
||||||
|
setEditContent(task);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div class="group fixed right-[22rem] bottom-4 hidden flex-row items-center justify-start space-x-3 overflow-x-hidden py-2 md:flex">
|
<div class="group fixed right-[22rem] bottom-4 hidden flex-row items-center justify-start space-x-3 overflow-x-hidden py-2 md:flex">
|
||||||
<div class="flex aspect-square h-20 cursor-pointer items-center justify-center rounded-full bg-[rgb(251,194,199,0.53)] text-9xl text-white shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-all duration-300 ease-out group-hover:ml-[12rem] hover:bg-[rgb(251,194,199,0.7)]">
|
<div
|
||||||
|
class="flex aspect-square h-20 cursor-pointer items-center justify-center rounded-full bg-[rgb(251,194,199,0.53)] text-9xl text-white shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-all duration-300 ease-out group-hover:ml-[12rem] hover:bg-[rgb(251,194,199,0.7)]"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCreating(true);
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute left-0 my-auto flex flex-row space-x-3 opacity-0 transition-opacity duration-100 group-hover:opacity-100">
|
<div class="absolute left-0 my-auto flex flex-row space-x-3 opacity-0 transition-opacity duration-100 group-hover:opacity-100">
|
||||||
@@ -43,7 +242,13 @@ const ProfileTasks: FunctionComponent = () => {
|
|||||||
<>
|
<>
|
||||||
<div class="flex w-full flex-1 flex-col items-center justify-center text-2xl">Начни уже сегодня!</div>
|
<div class="flex w-full flex-1 flex-col items-center justify-center text-2xl">Начни уже сегодня!</div>
|
||||||
<div class="fixed right-[22rem] bottom-4 hidden flex-row items-center justify-start overflow-x-hidden py-2 md:flex">
|
<div class="fixed right-[22rem] bottom-4 hidden flex-row items-center justify-start overflow-x-hidden py-2 md:flex">
|
||||||
<div class="flex aspect-square h-20 cursor-pointer items-center justify-center rounded-full bg-[rgb(251,194,199,0.53)] text-9xl text-white shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-all duration-300 ease-out hover:bg-[rgb(251,194,199,0.7)]">
|
<div
|
||||||
|
class="flex aspect-square h-20 cursor-pointer items-center justify-center rounded-full bg-[rgb(251,194,199,0.53)] text-9xl text-white shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-all duration-300 ease-out hover:bg-[rgb(251,194,199,0.7)]"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCreating(true);
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user