Compare commits
35 Commits
1eeaf31e52
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d4858bcbb | |||
| b2feaac3e1 | |||
| db9132de4d | |||
| ae9bea6c7c | |||
| 7f80f4790d | |||
| 5d11cf0dcd | |||
| 3a64f039af | |||
| 2e908d01f1 | |||
| 9cd3acd73c | |||
| 7c6f21081f | |||
| e6ab12f957 | |||
| a6eec16309 | |||
| 2ef4b967f3 | |||
| 083a0f27c0 | |||
| 554ee083ab | |||
| 04347f4cd9 | |||
| 6a6b8091c7 | |||
| b9efdef024 | |||
| ac9bde2a54 | |||
| 6c1efe702c | |||
| a6a145f712 | |||
| bab4aa1ddb | |||
| 48ef8c495e | |||
| fe9d21c218 | |||
| 454dc23245 | |||
| 38da06643c | |||
| 02ee281ae5 | |||
| c2dd2e89b3 | |||
| cbb28fac6f | |||
| cd618e4d1d | |||
| 0843e4b20d | |||
| f3c983be6f | |||
| a57c59bda5 | |||
| edda3f7443 | |||
| 78117e3421 |
10
bun.lock
10
bun.lock
@@ -4,12 +4,14 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "anti-hvost",
|
"name": "anti-hvost",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
"@preact/signals": "^2.0.2",
|
"@preact/signals": "^2.0.2",
|
||||||
"@tailwindcss/postcss": "^4.0.17",
|
"@tailwindcss/postcss": "^4.0.17",
|
||||||
"@tailwindcss/vite": "^4.0.17",
|
"@tailwindcss/vite": "^4.0.17",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"preact": "^10.26.2",
|
"preact": "^10.26.2",
|
||||||
|
"preact-iso": "^2.9.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",
|
||||||
@@ -146,6 +148,8 @@
|
|||||||
|
|
||||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="],
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="],
|
||||||
|
|
||||||
|
"@heroicons/react": ["@heroicons/react@2.2.0", "", { "peerDependencies": { "react": ">= 16 || ^19.0.0-rc" } }, "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ=="],
|
||||||
|
|
||||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||||
|
|
||||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||||
@@ -564,6 +568,10 @@
|
|||||||
|
|
||||||
"preact": ["preact@10.26.4", "", {}, "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w=="],
|
"preact": ["preact@10.26.4", "", {}, "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w=="],
|
||||||
|
|
||||||
|
"preact-iso": ["preact-iso@2.9.1", "", { "peerDependencies": { "preact": ">=10", "preact-render-to-string": ">=6.4.0" } }, "sha512-65+oY5FHeSC3mjq2Dg6v72HYjsTYTqI9hXl1BK6vtUkxwfCqDJ5aqk33zjOH3aSuCY7duWiulyqMQ53GXpPIIQ=="],
|
||||||
|
|
||||||
|
"preact-render-to-string": ["preact-render-to-string@6.5.13", "", { "peerDependencies": { "preact": ">=10" } }, "sha512-iGPd+hKPMFKsfpR2vL4kJ6ZPcFIoWZEcBf0Dpm3zOpdVvj77aY8RlLiQji5OMrngEyaxGogeakTb54uS2FvA6w=="],
|
||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
||||||
@@ -578,6 +586,8 @@
|
|||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
|
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
"require-relative": ["require-relative@0.8.7", "", {}, "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg=="],
|
"require-relative": ["require-relative@0.8.7", "", {}, "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg=="],
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + Preact + TS</title>
|
<title>Антихвост</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "anti-hvost",
|
"name": "anti-hvost",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -9,12 +9,14 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
"@preact/signals": "^2.0.2",
|
"@preact/signals": "^2.0.2",
|
||||||
"@tailwindcss/postcss": "^4.0.17",
|
"@tailwindcss/postcss": "^4.0.17",
|
||||||
"@tailwindcss/vite": "^4.0.17",
|
"@tailwindcss/vite": "^4.0.17",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.3",
|
||||||
"preact": "^10.26.2",
|
"preact": "^10.26.2",
|
||||||
|
"preact-iso": "^2.9.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"
|
||||||
|
|||||||
34
src/app.tsx
34
src/app.tsx
@@ -1,13 +1,31 @@
|
|||||||
import { useSignal } from "@preact/signals";
|
import { FunctionComponent } from "preact";
|
||||||
|
import { ErrorBoundary, lazy, LocationProvider, Route, Router, useLocation } from "preact-iso";
|
||||||
import "preact/debug";
|
import "preact/debug";
|
||||||
import classes from "./app.module.scss";
|
import Page404 from "./pages/404";
|
||||||
import Button from "./components/ui/Button";
|
import LoginPage from "./pages/login";
|
||||||
|
import { AppProvider, useAppContext } from "./providers/AuthProvider";
|
||||||
|
|
||||||
|
const HomePage: FunctionComponent = () => {
|
||||||
|
const { route } = useLocation();
|
||||||
|
const { isLoggedIn } = useAppContext();
|
||||||
|
if (isLoggedIn.value) route("/profile/tasks", true);
|
||||||
|
else route("/login", true);
|
||||||
|
return <div>Redirecting...</div>;
|
||||||
|
};
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const counter = useSignal(0);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<AppProvider>
|
||||||
<h1 class={classes.text}>Hello, World!</h1>
|
<LocationProvider>
|
||||||
<Button onClick={() => counter.value++}>Count: {counter.value}</Button>
|
<ErrorBoundary>
|
||||||
</>
|
<Router>
|
||||||
|
<Route path="/" component={HomePage} />
|
||||||
|
<Route path="/login" component={LoginPage} />
|
||||||
|
<Route path="/profile/*" component={lazy(() => import("./pages/profile"))} />
|
||||||
|
<Route default component={() => <Page404 />} />
|
||||||
|
</Router>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</LocationProvider>
|
||||||
|
</AppProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/components/menu.module.scss
Normal file
13
src/components/menu.module.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
@reference "../index.scss";
|
||||||
|
|
||||||
|
#container {
|
||||||
|
@apply flex min-h-auto w-screen flex-col items-center gap-4 px-2 py-3 md:min-h-screen md:w-[30rem];
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu_container {
|
||||||
|
@apply fixed right-0 bottom-0 left-0 flex h-[4rem] w-full flex-1 flex-row items-center gap-1 rounded-[44px] bg-[rgba(167,213,246,0.3)] px-1 py-5 md:sticky md:top-0 md:right-0 md:bottom-auto md:left-auto md:h-full md:flex-col md:gap-4 md:border-t-0 md:px-5 md:py-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu_item {
|
||||||
|
@apply flex w-full cursor-pointer flex-row items-center gap-3 rounded-full px-3 py-2 text-center hover:bg-gray-200 md:px-6;
|
||||||
|
}
|
||||||
99
src/components/menu.tsx
Normal file
99
src/components/menu.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { cn } from "@/utils/class-merge";
|
||||||
|
import { CalendarDaysIcon, ListBulletIcon, UserIcon } from "@heroicons/react/24/solid";
|
||||||
|
import { FunctionComponent, h } from "preact";
|
||||||
|
import { useLocation } from "preact-iso";
|
||||||
|
import { tv } from "tailwind-variants";
|
||||||
|
import classes from "./menu.module.scss";
|
||||||
|
|
||||||
|
interface MenuItemProps {
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
icon: h.JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuItem: FunctionComponent<MenuItemProps> = ({ title, link, icon }: MenuItemProps) => {
|
||||||
|
const { route, path } = useLocation();
|
||||||
|
const active = path === link;
|
||||||
|
const menuItemClasses = tv({
|
||||||
|
base: classes.menu_item,
|
||||||
|
variants: {
|
||||||
|
activity: {
|
||||||
|
active: "bg-gray-200",
|
||||||
|
inactive: "bg-white",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
activity: "inactive",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div class={menuItemClasses({ activity: active ? "active" : "inactive" })} onClick={() => route(link, true)}>
|
||||||
|
{icon}
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Avatar: FunctionComponent = () => {
|
||||||
|
const { route, path } = useLocation();
|
||||||
|
//TODO: Move styles to scss module
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => route("/profile/settings")}
|
||||||
|
class={cn("hidden h-32 w-full cursor-pointer overflow-hidden transition-[height] md:block", {
|
||||||
|
"h-0": path === "/profile/settings",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class={cn(
|
||||||
|
"h-full flex-row items-center justify-around rounded-[44px] bg-[linear-gradient(180.00deg,rgba(249,134,143,0.5)_3.053%,rgb(228,242,252)_96.183%)] px-5 py-5 md:flex"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="my-5 aspect-square h-full rounded-full bg-white"></div>
|
||||||
|
<div class="flex flex-col items-center justify-center">
|
||||||
|
<p class="text-3xl font-semibold">Никнейм</p>
|
||||||
|
<p class="text-xl font-light">Статус</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MenuItems {
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
icon: h.JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Menu: FunctionComponent = () => {
|
||||||
|
//TODO: Move links to enum
|
||||||
|
const menu_items: MenuItems[] = [
|
||||||
|
{
|
||||||
|
title: "Задачи",
|
||||||
|
link: "/profile/tasks",
|
||||||
|
icon: <ListBulletIcon class="size-10" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Календарь",
|
||||||
|
link: "/profile/calendar",
|
||||||
|
icon: <CalendarDaysIcon class="size-10" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Профиль",
|
||||||
|
link: "/profile/settings",
|
||||||
|
icon: <UserIcon class="size-10" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div id={classes.container}>
|
||||||
|
<Avatar />
|
||||||
|
<div class={classes.menu_container}>
|
||||||
|
{menu_items.map(({ title, link, icon }) => (
|
||||||
|
<MenuItem title={title} link={link} icon={icon} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Menu;
|
||||||
5
src/components/task.module.scss
Normal file
5
src/components/task.module.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@reference "../index.scss";
|
||||||
|
|
||||||
|
.task {
|
||||||
|
@apply flex h-24 w-[300px] cursor-pointer flex-row items-center justify-start gap-4 rounded-[39px] bg-[rgba(251,194,199,0.53)] px-5 py-6 text-xl shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-transform hover:scale-[1.05] hover:bg-[rgba(251,194,199,0.7)] active:scale-[1.05] md:w-[500px];
|
||||||
|
}
|
||||||
20
src/components/task.tsx
Normal file
20
src/components/task.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { FunctionComponent } from "preact";
|
||||||
|
import classes from "./task.module.scss";
|
||||||
|
|
||||||
|
interface TaskProps {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Task: FunctionComponent<TaskProps> = ({ name }: TaskProps) => {
|
||||||
|
return (
|
||||||
|
// Временное действие для тестирования
|
||||||
|
<button onClick={() => alert(name)}>
|
||||||
|
<div class={classes.task}>
|
||||||
|
<div class="aspect-square h-full rounded-full border bg-white"></div>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Task;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
@reference '../../index.scss';
|
@reference '../../index.scss';
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@apply rounded-2xl border-2 border-black p-5 text-white;
|
@apply rounded-[23px] px-4 py-3 text-xl text-black shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] transition-colors hover:cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,21 @@ const button = tv({
|
|||||||
base: classes.button,
|
base: classes.button,
|
||||||
variants: {
|
variants: {
|
||||||
color: {
|
color: {
|
||||||
primary: "bg-blue-400",
|
primary: "bg-[rgba(206,232,251,0.7)] hover:bg-[rgba(206,232,251,0.9)]",
|
||||||
secondary: "bg-red-400",
|
secondary: "bg-[rgba(255,251,197,0.68)] hover:bg-[rgba(255,251,197,0.9)]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const Button: FunctionComponent<{ onClick: () => void }> = (props) => {
|
interface ButtonProps {
|
||||||
|
color?: "primary" | "secondary";
|
||||||
|
onClick?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button: FunctionComponent<ButtonProps> = ({ children, onClick = () => {}, color = "primary" }) => {
|
||||||
return (
|
return (
|
||||||
<button type="button" class={button({ color: "primary" })} onClick={props.onClick}>
|
<button type="button" class={button({ color: color })} onClick={onClick}>
|
||||||
{props.children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
5
src/components/ui/Input.module.scss
Normal file
5
src/components/ui/Input.module.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@reference "../../index.scss";
|
||||||
|
|
||||||
|
.input_field {
|
||||||
|
@apply rounded-[23px] border border-gray-300 bg-white p-2 leading-8 placeholder:transition focus:outline-0 focus:placeholder:opacity-25;
|
||||||
|
}
|
||||||
34
src/components/ui/Input.tsx
Normal file
34
src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { FunctionComponent } from "preact";
|
||||||
|
import { tv } from "tailwind-variants";
|
||||||
|
import classes from "./Input.module.scss";
|
||||||
|
|
||||||
|
const input = tv({
|
||||||
|
base: classes.input_field,
|
||||||
|
variants: {
|
||||||
|
"text-align": {
|
||||||
|
center: "text-center",
|
||||||
|
left: "text-left",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
"text-align": "left",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface InputProps {
|
||||||
|
isPassword?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
textAlign?: "center" | "left";
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input: FunctionComponent<InputProps> = ({ isPassword = false, placeholder = "", textAlign }: InputProps) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={isPassword ? "password" : "text"}
|
||||||
|
class={input({ "text-align": textAlign })}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Input;
|
||||||
12
src/constructors/Component.tsx
Normal file
12
src/constructors/Component.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { FunctionComponent } from "preact";
|
||||||
|
import { useEffect } from "preact/hooks";
|
||||||
|
|
||||||
|
export const withTitle = <P,>(title: string, WrappedComponent: FunctionComponent<P>): FunctionComponent<P> => {
|
||||||
|
const ComponentWithTitle: FunctionComponent<P> = (props) => {
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = title;
|
||||||
|
}, []);
|
||||||
|
return <WrappedComponent {...props} />;
|
||||||
|
};
|
||||||
|
return ComponentWithTitle;
|
||||||
|
};
|
||||||
7
src/enums/urls.ts
Normal file
7
src/enums/urls.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export enum UrlsTitle {
|
||||||
|
LOGIN = "Авторизация",
|
||||||
|
PROFILE = "Профиль",
|
||||||
|
TASKS = "Задачи",
|
||||||
|
CALENDAR = "Календарь",
|
||||||
|
PAGE404 = "404",
|
||||||
|
}
|
||||||
@@ -1 +1,21 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import url("https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap");
|
||||||
|
:root {
|
||||||
|
font-family: "Montserrat", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
@apply w-2;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
@apply bg-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
@apply rounded-full bg-gray-300;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
@apply bg-gray-500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
9
src/pages/404.module.scss
Normal file
9
src/pages/404.module.scss
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
@reference "../index.scss";
|
||||||
|
|
||||||
|
#container {
|
||||||
|
@apply flex h-screen w-full flex-col items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main_container {
|
||||||
|
@apply flex flex-col items-center gap-8;
|
||||||
|
}
|
||||||
27
src/pages/404.tsx
Normal file
27
src/pages/404.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import Button from "@/components/ui/Button";
|
||||||
|
import { withTitle } from "@/constructors/Component";
|
||||||
|
import { UrlsTitle } from "@/enums/urls";
|
||||||
|
import { FunctionComponent } from "preact";
|
||||||
|
import { useLocation } from "preact-iso";
|
||||||
|
import classes from "./404.module.scss";
|
||||||
|
|
||||||
|
const Page404: FunctionComponent = () => {
|
||||||
|
const { route } = useLocation();
|
||||||
|
return (
|
||||||
|
<div id={classes.container}>
|
||||||
|
<div id={classes.main_container}>
|
||||||
|
<p class="text-6xl font-semibold">404</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
route("/", true);
|
||||||
|
}}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
На главную
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTitle(UrlsTitle.PAGE404, Page404);
|
||||||
1
src/pages/calendar.module.scss
Normal file
1
src/pages/calendar.module.scss
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@reference "../index.scss";
|
||||||
252
src/pages/calendar.tsx
Normal file
252
src/pages/calendar.tsx
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import { withTitle } from "@/constructors/Component";
|
||||||
|
import { UrlsTitle } from "@/enums/urls";
|
||||||
|
import { cn } from "@/utils/class-merge";
|
||||||
|
import { FunctionComponent, h } from "preact";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
|
||||||
|
type MarkedDateType = "event" | "holiday" | "important" | string;
|
||||||
|
type MarkedDates = Record<string, MarkedDateType>;
|
||||||
|
|
||||||
|
interface BigCalendarProps {
|
||||||
|
onDateSelect?: (date: Date) => void;
|
||||||
|
markedDates?: MarkedDates;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BigCalendar: FunctionComponent<BigCalendarProps> = ({
|
||||||
|
onDateSelect = () => {},
|
||||||
|
markedDates = {},
|
||||||
|
className = "",
|
||||||
|
}: BigCalendarProps) => {
|
||||||
|
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
||||||
|
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
const monthNames: string[] = [
|
||||||
|
"Январь",
|
||||||
|
"Февраль",
|
||||||
|
"Март",
|
||||||
|
"Апрель",
|
||||||
|
"Май",
|
||||||
|
"Июнь",
|
||||||
|
"Июль",
|
||||||
|
"Август",
|
||||||
|
"Сентябрь",
|
||||||
|
"Октябрь",
|
||||||
|
"Ноябрь",
|
||||||
|
"Декабрь",
|
||||||
|
];
|
||||||
|
|
||||||
|
const dayNames: string[] = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
|
||||||
|
|
||||||
|
const getDaysInMonth = (year: number, month: number): number => {
|
||||||
|
return new Date(year, month + 1, 0).getDate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFirstDayOfMonth = (year: number, month: number): number => {
|
||||||
|
const day = new Date(year, month, 1).getDay();
|
||||||
|
return day === 0 ? 6 : day - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevMonth = (): void => {
|
||||||
|
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextMonth = (): void => {
|
||||||
|
setCurrentDate(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateClick = (date: Date, isCurrentMonth: boolean): void => {
|
||||||
|
if (!isCurrentMonth) {
|
||||||
|
setCurrentDate(new Date(date.getFullYear(), date.getMonth(), 1));
|
||||||
|
}
|
||||||
|
setSelectedDate(date);
|
||||||
|
onDateSelect?.(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDateMarked = (date: Date): MarkedDateType | undefined => {
|
||||||
|
const dateStr = date.toISOString().split("T")[0];
|
||||||
|
return markedDates[dateStr];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLastDayOfMonth = (year: number, month: number): number => {
|
||||||
|
const day = new Date(year, month + 1, 0).getDay();
|
||||||
|
return day === 0 ? 6 : day - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDays = (): h.JSX.Element[] => {
|
||||||
|
const year: number = currentDate.getFullYear();
|
||||||
|
const month: number = currentDate.getMonth();
|
||||||
|
|
||||||
|
const daysInMonth: number = getDaysInMonth(year, month);
|
||||||
|
const firstDayOfMonth: number = getFirstDayOfMonth(year, month);
|
||||||
|
const lastDayOfMonth: number = getLastDayOfMonth(year, month);
|
||||||
|
|
||||||
|
const days: h.JSX.Element[] = [];
|
||||||
|
|
||||||
|
// Дни предыдущего месяца
|
||||||
|
//TODO: work on click on 31 march
|
||||||
|
const prevMonthDays = getDaysInMonth(year, month - 1);
|
||||||
|
const daysFromPrevMonth = firstDayOfMonth === 0 ? 6 : firstDayOfMonth;
|
||||||
|
for (let i = daysFromPrevMonth - 1; i >= 0; i--) {
|
||||||
|
const day = prevMonthDays - i;
|
||||||
|
const date = new Date(year, month - 1, day + 1);
|
||||||
|
const dateStr = date.toISOString().split("T")[0];
|
||||||
|
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
|
||||||
|
const markType = isDateMarked(date);
|
||||||
|
|
||||||
|
days.push(
|
||||||
|
<div
|
||||||
|
key={`prev-${day}`}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-24 cursor-pointer flex-col border border-gray-200 p-2 opacity-50 hover:opacity-70",
|
||||||
|
{ "bg-gray-200": isSelected }
|
||||||
|
)}
|
||||||
|
onClick={() => handleDateClick(date, false)}
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center self-end rounded-full text-gray-600">{day}</div>
|
||||||
|
{markType && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mb-1 truncate rounded p-1 text-xs",
|
||||||
|
{ "bg-green-100 text-green-800": markType === "event" },
|
||||||
|
{ "bg-red-100 text-red-800": markType === "holiday" },
|
||||||
|
{ "bg-yellow-100 text-yellow-800": markType === "important" }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{markType}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дни текущего месяца
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
const date = new Date(year, month, day + 1);
|
||||||
|
const dateStr = date.toISOString().split("T")[0];
|
||||||
|
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
|
||||||
|
const markType = isDateMarked(date);
|
||||||
|
const isToday = new Date().toISOString().split("T")[0] === dateStr;
|
||||||
|
|
||||||
|
days.push(
|
||||||
|
<div
|
||||||
|
key={`current-${day}`}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-24 flex-col border border-gray-200 p-2",
|
||||||
|
{ "border-blue-400 bg-blue-100": isSelected },
|
||||||
|
{ "border-yellow-400": isToday },
|
||||||
|
"cursor-pointer hover:bg-gray-50"
|
||||||
|
)}
|
||||||
|
onClick={() => (selectedDate ? setSelectedDate(null) : handleDateClick(date, true))}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-8 w-8 items-center justify-center self-end rounded-full",
|
||||||
|
{ "bg-blue-600 text-white": isSelected },
|
||||||
|
{ "bg-yellow-100 text-yellow-800": isToday && !isSelected }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{markType && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mb-1 truncate rounded p-1 text-xs",
|
||||||
|
{ "bg-green-100 text-green-800": markType === "event" },
|
||||||
|
{ "bg-red-100 text-red-800": markType === "holiday" },
|
||||||
|
{ "bg-yellow-100 text-yellow-800": markType === "important" }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{markType}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Дни следующего месяца
|
||||||
|
const daysToAdd = 6 - (lastDayOfMonth === 6 ? 4 : lastDayOfMonth);
|
||||||
|
for (let day = 1; day <= daysToAdd; day++) {
|
||||||
|
const date = new Date(year, month + 1, day + 1);
|
||||||
|
const dateStr = date.toISOString().split("T")[0];
|
||||||
|
const isSelected = selectedDate?.toISOString().split("T")[0] === dateStr;
|
||||||
|
const markType = isDateMarked(date);
|
||||||
|
|
||||||
|
days.push(
|
||||||
|
<div
|
||||||
|
key={`next-${day}`}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-24 cursor-pointer flex-col border border-gray-200 p-2 opacity-50 hover:opacity-70",
|
||||||
|
{ "bg-gray-200": isSelected }
|
||||||
|
)}
|
||||||
|
onClick={() => handleDateClick(date, false)}
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center self-end rounded-full text-gray-600">{day}</div>
|
||||||
|
{markType && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mb-1 truncate rounded p-1 text-xs",
|
||||||
|
{ "bg-green-100 text-green-800": markType === "event" },
|
||||||
|
{ "bg-red-100 text-red-800": markType === "holiday" },
|
||||||
|
{ "bg-yellow-100 text-yellow-800": markType === "important" }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{markType}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col ${className}`}>
|
||||||
|
<div className="mb-4 flex items-center justify-between px-2">
|
||||||
|
<button
|
||||||
|
onClick={handlePrevMonth}
|
||||||
|
className="rounded-lg p-2 text-gray-700 hover:bg-gray-100"
|
||||||
|
aria-label="Previous month"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<h2 className="text-xl font-bold text-gray-800">
|
||||||
|
{monthNames[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={handleNextMonth}
|
||||||
|
className="rounded-lg p-2 text-gray-700 hover:bg-gray-100"
|
||||||
|
aria-label="Next month"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-2 grid grid-cols-7 gap-1 text-center text-sm font-medium text-gray-500">
|
||||||
|
{dayNames.map((day) => (
|
||||||
|
<div key={day} className="flex h-10 items-center justify-center">
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid flex-1 grid-cols-7 gap-1">{renderDays()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTitle(UrlsTitle.CALENDAR, BigCalendar);
|
||||||
13
src/pages/login.module.scss
Normal file
13
src/pages/login.module.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
@reference "../index.scss";
|
||||||
|
|
||||||
|
.login_container {
|
||||||
|
@apply flex min-h-screen flex-col items-center justify-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login_card {
|
||||||
|
@apply flex min-h-[50vh] w-[95%] min-w-[300px] flex-col justify-around gap-2 rounded-[103px] bg-[linear-gradient(180.00deg,_rgba(239,251,194,0.53),rgb(206,232,251)_100%)] p-7 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)] md:w-[350px];
|
||||||
|
}
|
||||||
|
|
||||||
|
.login_card_name {
|
||||||
|
@apply text-center text-2xl font-semibold;
|
||||||
|
}
|
||||||
33
src/pages/login.tsx
Normal file
33
src/pages/login.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import Button from "@/components/ui/Button";
|
||||||
|
import Input from "@/components/ui/Input";
|
||||||
|
import { withTitle } from "@/constructors/Component";
|
||||||
|
import { UrlsTitle } from "@/enums/urls";
|
||||||
|
import { useAppContext } from "@/providers/AuthProvider";
|
||||||
|
import { FunctionComponent } from "preact";
|
||||||
|
import { useLocation } from "preact-iso";
|
||||||
|
import classes from "./login.module.scss";
|
||||||
|
const LoginPage: FunctionComponent = () => {
|
||||||
|
const { isLoggedIn } = useAppContext();
|
||||||
|
const { route } = useLocation();
|
||||||
|
return (
|
||||||
|
<div class={classes.login_container}>
|
||||||
|
<div class={classes.login_card}>
|
||||||
|
<p class={classes.login_card_name}>Антихвост</p>
|
||||||
|
<Input placeholder="Логин" textAlign="center" />
|
||||||
|
<Input isPassword placeholder="Пароль" textAlign="center" />
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
isLoggedIn.value = true;
|
||||||
|
localStorage.setItem("loggedIn", "true");
|
||||||
|
route("/profile/tasks", true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Войти
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTitle(UrlsTitle.LOGIN, LoginPage);
|
||||||
13
src/pages/profile.module.scss
Normal file
13
src/pages/profile.module.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
@reference "../index.scss";
|
||||||
|
|
||||||
|
#main_container {
|
||||||
|
@apply flex h-screen flex-col md:flex-row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#router_container {
|
||||||
|
@apply flex-1 overflow-y-auto px-3 pb-[6rem] break-all md:pb-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#menu_container {
|
||||||
|
@apply md:sticky md:top-0 md:h-screen;
|
||||||
|
}
|
||||||
36
src/pages/profile.tsx
Normal file
36
src/pages/profile.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import Menu from "@/components/menu";
|
||||||
|
import { useAppContext } from "@/providers/AuthProvider";
|
||||||
|
import { FunctionComponent } from "preact";
|
||||||
|
import { lazy, Route, Router, useLocation } from "preact-iso";
|
||||||
|
import ids from "./profile.module.scss";
|
||||||
|
|
||||||
|
const ProfilePage: FunctionComponent = () => {
|
||||||
|
const { route } = useLocation();
|
||||||
|
const { isLoggedIn } = useAppContext();
|
||||||
|
if (!isLoggedIn.value) route("/login", true);
|
||||||
|
return isLoggedIn.value ? (
|
||||||
|
<div id={ids.main_container}>
|
||||||
|
<div id={ids.router_container}>
|
||||||
|
<Router>
|
||||||
|
<Route path="/settings" component={lazy(() => import("./profile_settings"))} />
|
||||||
|
<Route path="/tasks" component={lazy(() => import("./profile_tasks"))} />
|
||||||
|
<Route path="/calendar" component={lazy(() => import("./calendar"))} />
|
||||||
|
<Route
|
||||||
|
default
|
||||||
|
component={() => {
|
||||||
|
route("/profile/tasks", true);
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Router>
|
||||||
|
</div>
|
||||||
|
<div id={ids.menu_container}>
|
||||||
|
<Menu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p>Redirecting...</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProfilePage;
|
||||||
24
src/pages/profile_settings.module.scss
Normal file
24
src/pages/profile_settings.module.scss
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
@reference "../index.scss";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
@apply flex h-full w-full flex-col items-center px-6 pt-3 md:flex-row md:items-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
#avatar {
|
||||||
|
@apply flex aspect-square h-40 flex-col items-center justify-center rounded-md border;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile_container {
|
||||||
|
@apply flex h-full w-full flex-col items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings_block__buttons {
|
||||||
|
@apply flex w-[300px] flex-col gap-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header_block__name {
|
||||||
|
@apply mt-5 flex h-fit w-full flex-col items-center justify-start gap-3;
|
||||||
|
}
|
||||||
|
.settings_block {
|
||||||
|
@apply mt-12 flex h-full w-full flex-col items-center justify-start md:mt-0 md:justify-center;
|
||||||
|
}
|
||||||
42
src/pages/profile_settings.tsx
Normal file
42
src/pages/profile_settings.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import Button from "@/components/ui/Button";
|
||||||
|
import { withTitle } from "@/constructors/Component";
|
||||||
|
import { UrlsTitle } from "@/enums/urls";
|
||||||
|
import { useAppContext } from "@/providers/AuthProvider";
|
||||||
|
import { FunctionComponent } from "preact";
|
||||||
|
import { useLocation } from "preact-iso";
|
||||||
|
import classes from "./profile_settings.module.scss";
|
||||||
|
|
||||||
|
const ProfileSettings: FunctionComponent = () => {
|
||||||
|
const { isLoggedIn } = useAppContext();
|
||||||
|
const { route } = useLocation();
|
||||||
|
return (
|
||||||
|
<div class={classes.container}>
|
||||||
|
<div id={classes.avatar}>Аватар</div>
|
||||||
|
<div class={classes.profile_container}>
|
||||||
|
<div class={classes.header_block__name}>
|
||||||
|
<p class="text-5xl font-semibold">Никнейм</p>
|
||||||
|
<p class="text-2xl font-light">Статус</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class={classes.settings_block}>
|
||||||
|
<div class={classes.settings_block__buttons}>
|
||||||
|
<Button>Сменить тему</Button>
|
||||||
|
<Button>Настройки</Button>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
isLoggedIn.value = false;
|
||||||
|
localStorage.setItem("loggedIn", "false");
|
||||||
|
route("/login", true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Выйти
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTitle(UrlsTitle.PROFILE, ProfileSettings);
|
||||||
13
src/pages/profile_tasks.module.scss
Normal file
13
src/pages/profile_tasks.module.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
@reference "../index.scss";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
@apply flex h-fit w-full flex-col items-center gap-4 px-6 pt-3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
@apply mb-12 w-full text-3xl font-semibold md:text-5xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tasks_container {
|
||||||
|
@apply flex w-full flex-col items-center gap-10 md:items-start;
|
||||||
|
}
|
||||||
28
src/pages/profile_tasks.tsx
Normal file
28
src/pages/profile_tasks.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Task from "@/components/task";
|
||||||
|
import { withTitle } from "@/constructors/Component";
|
||||||
|
import { UrlsTitle } from "@/enums/urls";
|
||||||
|
import { FunctionComponent } from "preact";
|
||||||
|
import { useMemo } from "preact/hooks";
|
||||||
|
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 ProfileTasks: FunctionComponent = () => {
|
||||||
|
const getDate = useMemo(() => {
|
||||||
|
const date = new Date();
|
||||||
|
const formatter = new Intl.DateTimeFormat("ru-RU", { month: "long", day: "numeric" });
|
||||||
|
return formatter.format(date);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div class={classes.container}>
|
||||||
|
<div class={classes.header}>Сегодня: {getDate}</div>
|
||||||
|
<div class={classes.tasks_container}>
|
||||||
|
{example_tasks.map((task, index) => (
|
||||||
|
<Task name={task} key={index} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTitle(UrlsTitle.TASKS, ProfileTasks);
|
||||||
31
src/providers/AuthProvider.tsx
Normal file
31
src/providers/AuthProvider.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { stringToBoolean } from "@/utils/converter";
|
||||||
|
import { signal, Signal } from "@preact/signals";
|
||||||
|
import { createContext, JSX } from "preact";
|
||||||
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
|
interface AppContextValue {
|
||||||
|
isLoggedIn: Signal<boolean>;
|
||||||
|
}
|
||||||
|
const ininitialValue = stringToBoolean(localStorage.getItem("loggedIn"));
|
||||||
|
|
||||||
|
const AppContext = createContext<AppContextValue>({
|
||||||
|
isLoggedIn: signal(ininitialValue),
|
||||||
|
});
|
||||||
|
|
||||||
|
const AppProvider = ({ children }: { children: JSX.Element }) => {
|
||||||
|
const value: AppContextValue = {
|
||||||
|
isLoggedIn: signal(ininitialValue),
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAppContext = () => {
|
||||||
|
const context = useContext(AppContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useAppContext must be used within AppProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { AppProvider, useAppContext };
|
||||||
5
src/utils/converter.ts
Normal file
5
src/utils/converter.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const stringToBoolean = (value: string | null): boolean => {
|
||||||
|
if (value === "true") return true;
|
||||||
|
if (value === "false") return false;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user