22 Commits

Author SHA1 Message Date
3d4858bcbb version: v0.0.1 2025-04-15 11:26:10 +03:00
b2feaac3e1 feat: withTitle modif 2025-04-15 11:23:48 +03:00
db9132de4d feat: pseudo-auth 2025-04-14 21:10:04 +03:00
ae9bea6c7c feat: pseudo-login 2025-04-14 21:01:33 +03:00
7f80f4790d feat: input styles 2025-04-14 20:45:46 +03:00
5d11cf0dcd feat: button styles 2025-04-14 20:45:34 +03:00
3a64f039af feat: task styles 2025-04-14 20:45:18 +03:00
2e908d01f1 feat: login styles 2025-04-14 20:45:07 +03:00
9cd3acd73c feat: menu styles 2025-04-09 12:11:09 +03:00
7c6f21081f feat: 404 page 2025-04-07 17:06:08 +03:00
e6ab12f957 feat: page titles 2025-04-07 17:06:01 +03:00
a6eec16309 feat: change default path after login 2025-04-07 16:38:17 +03:00
2ef4b967f3 feat: styles update 2025-04-07 16:37:59 +03:00
083a0f27c0 feat: scrollbar styles 2025-04-07 16:06:28 +03:00
554ee083ab feat: profile styles 2025-04-07 15:59:04 +03:00
04347f4cd9 feat: logout button 2025-04-07 13:03:21 +03:00
6a6b8091c7 feat: profile tasks page 2025-04-07 13:03:11 +03:00
b9efdef024 feat: task component 2025-04-07 13:02:50 +03:00
ac9bde2a54 feat: proto-profile page 2025-04-07 12:42:40 +03:00
6c1efe702c feat: some button style changes 2025-04-07 12:42:25 +03:00
a6a145f712 feat: calendar fixes 2025-04-03 16:51:56 +03:00
bab4aa1ddb feat: calendar screen 2025-04-03 16:17:49 +03:00
27 changed files with 575 additions and 112 deletions

View File

@@ -4,6 +4,7 @@
"": {
"name": "anti-hvost",
"dependencies": {
"@heroicons/react": "^2.2.0",
"@preact/signals": "^2.0.2",
"@tailwindcss/postcss": "^4.0.17",
"@tailwindcss/vite": "^4.0.17",
@@ -147,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=="],
"@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/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
@@ -583,6 +586,8 @@
"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=="],
"require-relative": ["require-relative@0.8.7", "", {}, "sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg=="],

View File

@@ -1,7 +1,7 @@
{
"name": "anti-hvost",
"private": true,
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@heroicons/react": "^2.2.0",
"@preact/signals": "^2.0.2",
"@tailwindcss/postcss": "^4.0.17",
"@tailwindcss/vite": "^4.0.17",

View File

@@ -1,13 +1,16 @@
import { FunctionComponent } from "preact";
import { ErrorBoundary, lazy, LocationProvider, Route, Router, useLocation } from "preact-iso";
import "preact/debug";
import Page404 from "./pages/404";
import LoginPage from "./pages/login";
import { AppProvider } from "./providers/AuthProvider";
import { AppProvider, useAppContext } from "./providers/AuthProvider";
const HomePage: FunctionComponent = () => {
const { route } = useLocation();
route("/login");
return <div>Redirecting to login...</div>;
const { isLoggedIn } = useAppContext();
if (isLoggedIn.value) route("/profile/tasks", true);
else route("/login", true);
return <div>Redirecting...</div>;
};
export function App() {
@@ -19,7 +22,7 @@ export function App() {
<Route path="/" component={HomePage} />
<Route path="/login" component={LoginPage} />
<Route path="/profile/*" component={lazy(() => import("./pages/profile"))} />
<Route default component={() => <h1>404</h1>} />
<Route default component={() => <Page404 />} />
</Router>
</ErrorBoundary>
</LocationProvider>

View File

@@ -1,9 +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 min-h-auto flex-row items-center gap-1 border-t border-t-gray-500 bg-white px-1 py-5 md:sticky md:top-0 md:right-0 md:bottom-auto md:left-auto md:min-h-screen md:w-auto md:flex-col md:gap-4 md:border-t-0 md:border-l md:border-l-gray-500 md:px-5 md:py-5;
@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 w-full cursor-pointer rounded-md px-3 py-2 text-center hover:bg-gray-200 md:px-6;
@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;
}

View File

@@ -1,4 +1,6 @@
import { FunctionComponent } from "preact";
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";
@@ -6,9 +8,10 @@ import classes from "./menu.module.scss";
interface MenuItemProps {
title: string;
link: string;
icon: h.JSX.Element;
}
const MenuItem: FunctionComponent<MenuItemProps> = ({ title, link }: MenuItemProps) => {
const MenuItem: FunctionComponent<MenuItemProps> = ({ title, link, icon }: MenuItemProps) => {
const { route, path } = useLocation();
const active = path === link;
const menuItemClasses = tv({
@@ -16,7 +19,7 @@ const MenuItem: FunctionComponent<MenuItemProps> = ({ title, link }: MenuItemPro
variants: {
activity: {
active: "bg-gray-200",
inactive: "bg-gray-100",
inactive: "bg-white",
},
},
defaultVariants: {
@@ -25,36 +28,70 @@ const MenuItem: FunctionComponent<MenuItemProps> = ({ title, link }: MenuItemPro
});
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/settings",
},
{
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 class={classes.menu_container}>
{menu_items.map(({ title, link }) => (
<MenuItem title={title} link={link} />
))}
<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>
);
};

View 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
View 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;

View File

@@ -1,5 +1,5 @@
@reference '../../index.scss';
.button {
@apply rounded-2xl border-2 py-3 font-semibold text-white transition-colors hover:cursor-pointer;
@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;
}

View File

@@ -5,18 +5,18 @@ const button = tv({
base: classes.button,
variants: {
color: {
primary: "bg-blue-400 hover:bg-blue-500",
secondary: "bg-red-400 hover:bg-red-500",
primary: "bg-[rgba(206,232,251,0.7)] hover:bg-[rgba(206,232,251,0.9)]",
secondary: "bg-[rgba(255,251,197,0.68)] hover:bg-[rgba(255,251,197,0.9)]",
},
},
});
interface ButtonProps {
color?: "primary" | "secondary";
onClick: () => void;
onClick?: () => void;
}
const Button: FunctionComponent<ButtonProps> = ({ children, onClick, color = "primary" }) => {
const Button: FunctionComponent<ButtonProps> = ({ children, onClick = () => {}, color = "primary" }) => {
return (
<button type="button" class={button({ color: color })} onClick={onClick}>
{children}

View File

@@ -1,5 +1,5 @@
@reference "../../index.scss";
.input_field {
@apply rounded-md border border-gray-300 p-2 placeholder:transition focus:outline-0 focus:placeholder:opacity-25;
@apply rounded-[23px] border border-gray-300 bg-white p-2 leading-8 placeholder:transition focus:outline-0 focus:placeholder:opacity-25;
}

View 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
View File

@@ -0,0 +1,7 @@
export enum UrlsTitle {
LOGIN = "Авторизация",
PROFILE = "Профиль",
TASKS = "Задачи",
CALENDAR = "Календарь",
PAGE404 = "404",
}

View File

@@ -3,3 +3,19 @@
: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;
}
}

View 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
View 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);

View File

@@ -0,0 +1 @@
@reference "../index.scss";

252
src/pages/calendar.tsx Normal file
View 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);

View File

@@ -5,7 +5,7 @@
}
.login_card {
@apply flex w-[95%] min-w-[300px] flex-col justify-center gap-2 rounded-md border-gray-400 p-5 shadow-md md:w-[350px];
@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 {

View File

@@ -1,5 +1,7 @@
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";
@@ -11,19 +13,21 @@ const LoginPage: FunctionComponent = () => {
<div class={classes.login_container}>
<div class={classes.login_card}>
<p class={classes.login_card_name}>Антихвост</p>
<Input placeholder="Login" textAlign="center" />
<Input isPassword placeholder="Password" textAlign="center" />
<Input placeholder="Логин" textAlign="center" />
<Input isPassword placeholder="Пароль" textAlign="center" />
<Button
color="secondary"
onClick={() => {
isLoggedIn.value = true;
route("/profile/settings", true);
localStorage.setItem("loggedIn", "true");
route("/profile/tasks", true);
}}
>
Login
Войти
</Button>
</div>
</div>
);
};
export default LoginPage;
export default withTitle(UrlsTitle.LOGIN, LoginPage);

View File

@@ -1 +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;
}

View File

@@ -1,88 +1,20 @@
import Menu from "@/components/menu";
import { useAppContext } from "@/providers/AuthProvider";
import { FunctionComponent } from "preact";
import { Route, Router, useLocation } from "preact-iso";
import { lazy, Route, Router, useLocation } from "preact-iso";
import ids from "./profile.module.scss";
const ProfilePage: FunctionComponent = () => {
const { route } = useLocation();
return (
<div class="flex h-screen flex-col md:flex-row">
<div class="flex-1 overflow-y-auto px-3 pb-20 break-all md:pb-0">
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={() => (
<p class="text-2xl">
information sets circus station three entirely outer fun event free thirty breathing race snow planned
surface fully he cast town group plant egg journeyearly city thread fight outline wear key flame special
voice carefully exactly entire notice practical biggest television find particular middle noise cloud
tobacco cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight
son front motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us
broke warn prevent follow depth been buffalo bag classroom before term repeat want here plant division
camera hang preparechurch call member it build rose who potatoes easy sides exclaimed foot independent
point alone gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving
whale quick tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing
instant result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during
electricity available managed comfortable eight left scientist article which stove term information sets
circus station three entirely outer fun event free thirty breathing race snow planned surface fully he
cast town group plant egg journeyearly city thread fight outline wear key flame special voice carefully
exactly entire notice practical biggest television find particular middle noise cloud tobacco
cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight son front
motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us broke warn
prevent follow depth been buffalo bag classroom before term repeat want here plant division camera hang
preparechurch call member it build rose who potatoes easy sides exclaimed foot independent point alone
gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving whale quick
tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing instant
result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during electricity
available managed comfortable eight left scientist article which stove terminformation sets circus
station three entirely outer fun event free thirty breathing race snow planned surface fully he cast
town group plant egg journeyearly city thread fight outline wear key flame special voice carefully
exactly entire notice practical biggest television find particular middle noise cloud tobacco
cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight son front
motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us broke warn
prevent follow depth been buffalo bag classroom before term repeat want here plant division camera hang
preparechurch call member it build rose who potatoes easy sides exclaimed foot independent point alone
gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving whale quick
tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing instant
result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during electricity
available managed comfortable eight left scientist article which stove terminformation sets circus
station three entirely outer fun event free thirty breathing race snow planned surface fully he cast
town group plant egg journeyearly city thread fight outline wear key flame special voice carefully
exactly entire notice practical biggest television find particular middle noise cloud tobacco
cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight son front
motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us broke warn
prevent follow depth been buffalo bag classroom before term repeat want here plant division camera hang
preparechurch call member it build rose who potatoes easy sides exclaimed foot independent point alone
gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving whale quick
tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing instant
result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during electricity
available managed comfortable eight left scientist article which stove terminformation sets circus
station three entirely outer fun event free thirty breathing race snow planned surface fully he cast
town group plant egg journeyearly city thread fight outline wear key flame special voice carefully
exactly entire notice practical biggest television find particular middle noise cloud tobacco
cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight son front
motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us broke warn
prevent follow depth been buffalo bag classroom before term repeat want here plant division camera hang
preparechurch call member it build rose who potatoes easy sides exclaimed foot independent point alone
gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving whale quick
tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing instant
result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during electricity
available managed comfortable eight left scientist article which stove terminformation sets circus
station three entirely outer fun event free thirty breathing race snow planned surface fully he cast
town group plant egg journeyearly city thread fight outline wear key flame special voice carefully
exactly entire notice practical biggest television find particular middle noise cloud tobacco
cameraowner rule factor lift within order somewhere rocky powerful steam expect forty tonight son front
motor knowledge law easily equipment worse adult straw declaredhave famous choose ants us broke warn
prevent follow depth been buffalo bag classroom before term repeat want here plant division camera hang
preparechurch call member it build rose who potatoes easy sides exclaimed foot independent point alone
gone forty completely habit slept heard stand forest paperjoy repeat shot wrote label giving whale quick
tomorrow feathers additional active unless reader angle copy blanket white pitch my seeing instant
result asideexcited front word guess each wolf leader steady rhyme sound thy chapter during electricity
available managed comfortable eight left scientist article which stove term
</p>
)}
/>
<Route path="/tasks" component={() => <p class="text-2xl">Tasks</p>} />
<Route path="/calendar" component={() => <p class="text-2xl">Calendar</p>} />
<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={() => {
@@ -92,10 +24,12 @@ const ProfilePage: FunctionComponent = () => {
/>
</Router>
</div>
<div className="md:sticky md:top-0 md:h-screen">
<div id={ids.menu_container}>
<Menu />
</div>
</div>
) : (
<p>Redirecting...</p>
);
};

View 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;
}

View 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);

View 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;
}

View 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);

View File

@@ -1,3 +1,4 @@
import { stringToBoolean } from "@/utils/converter";
import { signal, Signal } from "@preact/signals";
import { createContext, JSX } from "preact";
import { useContext } from "preact/hooks";
@@ -5,14 +6,15 @@ import { useContext } from "preact/hooks";
interface AppContextValue {
isLoggedIn: Signal<boolean>;
}
const ininitialValue = stringToBoolean(localStorage.getItem("loggedIn"));
const AppContext = createContext<AppContextValue>({
isLoggedIn: signal(false),
isLoggedIn: signal(ininitialValue),
});
const AppProvider = ({ children }: { children: JSX.Element }) => {
const value: AppContextValue = {
isLoggedIn: signal(false),
isLoggedIn: signal(ininitialValue),
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;

5
src/utils/converter.ts Normal file
View File

@@ -0,0 +1,5 @@
export const stringToBoolean = (value: string | null): boolean => {
if (value === "true") return true;
if (value === "false") return false;
return false;
};