Compare commits
9 Commits
7c6f21081f
...
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d4858bcbb | |||
| b2feaac3e1 | |||
| db9132de4d | |||
| ae9bea6c7c | |||
| 7f80f4790d | |||
| 5d11cf0dcd | |||
| 3a64f039af | |||
| 2e908d01f1 | |||
| 9cd3acd73c |
5
bun.lock
5
bun.lock
@@ -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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { AppProvider, useAppContext } from "./providers/AuthProvider";
|
||||
const HomePage: FunctionComponent = () => {
|
||||
const { route } = useLocation();
|
||||
const { isLoggedIn } = useAppContext();
|
||||
if (isLoggedIn) route("/profile/tasks", true);
|
||||
if (isLoggedIn.value) route("/profile/tasks", true);
|
||||
else route("/login", true);
|
||||
return <div>Redirecting...</div>;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@reference "../index.scss";
|
||||
|
||||
.task {
|
||||
@apply flex h-24 w-[300px] cursor-pointer flex-col items-center justify-center rounded-md border-2 text-xl transition-transform hover:scale-[1.05] active:scale-[1.05] md:w-[500px];
|
||||
@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];
|
||||
}
|
||||
|
||||
@@ -9,7 +9,10 @@ const Task: FunctionComponent<TaskProps> = ({ name }: TaskProps) => {
|
||||
return (
|
||||
// Временное действие для тестирования
|
||||
<button onClick={() => alert(name)}>
|
||||
<div class={classes.task}>{name}</div>
|
||||
<div class={classes.task}>
|
||||
<div class="aspect-square h-full rounded-full border bg-white"></div>
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@reference '../../index.scss';
|
||||
|
||||
.button {
|
||||
@apply rounded-2xl border-2 px-4 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;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ 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)]",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
@@ -1,15 +1,12 @@
|
||||
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 { useEffect } from "preact/hooks";
|
||||
import classes from "./404.module.scss";
|
||||
|
||||
const Page404: FunctionComponent = () => {
|
||||
const { route } = useLocation();
|
||||
useEffect(() => {
|
||||
document.title = UrlsTitle.PAGE404;
|
||||
}, []);
|
||||
return (
|
||||
<div id={classes.container}>
|
||||
<div id={classes.main_container}>
|
||||
@@ -27,4 +24,4 @@ const Page404: FunctionComponent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Page404;
|
||||
export default withTitle(UrlsTitle.PAGE404, Page404);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { withTitle } from "@/constructors/Component";
|
||||
import { UrlsTitle } from "@/enums/urls";
|
||||
import { cn } from "@/utils/class-merge";
|
||||
import { FunctionComponent, h } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
type MarkedDateType = "event" | "holiday" | "important" | string;
|
||||
type MarkedDates = Record<string, MarkedDateType>;
|
||||
@@ -17,9 +18,6 @@ const BigCalendar: FunctionComponent<BigCalendarProps> = ({
|
||||
markedDates = {},
|
||||
className = "",
|
||||
}: BigCalendarProps) => {
|
||||
useEffect(() => {
|
||||
document.title = UrlsTitle.CALENDAR;
|
||||
}, []);
|
||||
const [currentDate, setCurrentDate] = useState<Date>(new Date());
|
||||
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
||||
|
||||
@@ -251,4 +249,4 @@ const BigCalendar: FunctionComponent<BigCalendarProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default BigCalendar;
|
||||
export default withTitle(UrlsTitle.CALENDAR, BigCalendar);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,34 +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 { useEffect } from "preact/hooks";
|
||||
import classes from "./login.module.scss";
|
||||
const LoginPage: FunctionComponent = () => {
|
||||
const { isLoggedIn } = useAppContext();
|
||||
const { route } = useLocation();
|
||||
useEffect(() => {
|
||||
document.title = UrlsTitle.LOGIN;
|
||||
}, []);
|
||||
return (
|
||||
<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;
|
||||
localStorage.setItem("loggedIn", "true");
|
||||
route("/profile/tasks", true);
|
||||
}}
|
||||
>
|
||||
Login
|
||||
Войти
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
export default withTitle(UrlsTitle.LOGIN, LoginPage);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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();
|
||||
return (
|
||||
const { isLoggedIn } = useAppContext();
|
||||
if (!isLoggedIn.value) route("/login", true);
|
||||
return isLoggedIn.value ? (
|
||||
<div id={ids.main_container}>
|
||||
<div id={ids.router_container}>
|
||||
<Router>
|
||||
@@ -25,6 +28,8 @@ const ProfilePage: FunctionComponent = () => {
|
||||
<Menu />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p>Redirecting...</p>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
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 { useEffect } from "preact/hooks";
|
||||
import classes from "./profile_settings.module.scss";
|
||||
|
||||
const ProfileSettings: FunctionComponent = () => {
|
||||
const { isLoggedIn } = useAppContext();
|
||||
const { route } = useLocation();
|
||||
useEffect(() => {
|
||||
document.title = UrlsTitle.PROFILE;
|
||||
}, []);
|
||||
return (
|
||||
<div class={classes.container}>
|
||||
<div id={classes.avatar}>Аватар</div>
|
||||
@@ -29,6 +26,7 @@ const ProfileSettings: FunctionComponent = () => {
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
isLoggedIn.value = false;
|
||||
localStorage.setItem("loggedIn", "false");
|
||||
route("/login", true);
|
||||
}}
|
||||
>
|
||||
@@ -41,4 +39,4 @@ const ProfileSettings: FunctionComponent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileSettings;
|
||||
export default withTitle(UrlsTitle.PROFILE, ProfileSettings);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Task from "@/components/task";
|
||||
import { withTitle } from "@/constructors/Component";
|
||||
import { UrlsTitle } from "@/enums/urls";
|
||||
import { FunctionComponent } from "preact";
|
||||
import { useEffect, useMemo } from "preact/hooks";
|
||||
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"];
|
||||
@@ -12,9 +13,6 @@ const ProfileTasks: FunctionComponent = () => {
|
||||
const formatter = new Intl.DateTimeFormat("ru-RU", { month: "long", day: "numeric" });
|
||||
return formatter.format(date);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
document.title = UrlsTitle.TASKS;
|
||||
}, []);
|
||||
return (
|
||||
<div class={classes.container}>
|
||||
<div class={classes.header}>Сегодня: {getDate}</div>
|
||||
@@ -27,4 +25,4 @@ const ProfileTasks: FunctionComponent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileTasks;
|
||||
export default withTitle(UrlsTitle.TASKS, ProfileTasks);
|
||||
|
||||
@@ -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
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