This commit is contained in:
2024-10-28 16:16:53 +05:00
parent 77aabed207
commit d6809ff538
30 changed files with 1250 additions and 658 deletions
+7 -10
View File
@@ -3,40 +3,37 @@ import useAuthStore from "../stores/useAuthStore";
import Button from "./Button";
function Header() {
const [accessToken, setAccessToken] = useAuthStore((state) => [
state.accessToken,
state.setAccessToken,
]);
const { user, setUser } = useAuthStore();
function logout() {
setAccessToken(null);
setUser(null);
}
return (
<div className="border-b border-neutral-300">
<div className="container mx-auto px-4 xl:max-w-screen-xl">
<div className="container px-4 mx-auto xl:max-w-screen-xl">
<div className="flex justify-between">
<div className="">
<a
href="/"
className="p-2 inline-block transition-colors hover:bg-neutral-200"
className="inline-block p-2 transition-colors hover:bg-neutral-200"
>
<span className="text-2xl">Logo</span>
</a>
</div>
<div className="">
{accessToken ? (
{user?.accessToken ? (
<Button
type="button"
handleClick={logout}
className="bg-transparent text-black hover:bg-neutral-200"
className="text-black bg-transparent hover:bg-neutral-200"
>
Logout
</Button>
) : (
<Link
to="/login"
className="p-3 inline-block transition-colors hover:bg-neutral-200"
className="inline-block p-3 transition-colors hover:bg-neutral-200"
>
Login
</Link>
+5 -5
View File
@@ -10,18 +10,18 @@ import { useClickAway } from "@uidotdev/usehooks";
function Menu() {
const [isShow, setIsShow] = useState<boolean>(false);
const { user, setAccessToken } = useAuthStore();
const { user, setUser } = useAuthStore();
const ref = useClickAway<HTMLDivElement>(() => {
setIsShow(false);
});
function logout() {
setAccessToken(null);
setUser(null);
}
return (
<div>
<span className="relative cursor-pointer z-20">
<span className="relative z-20 cursor-pointer">
<button
onClick={() => setIsShow(true)}
className={`p-3 transition-colors relative z-20 ${
@@ -37,7 +37,7 @@ function Menu() {
<Transition in={isShow} timeout={150} mountOnEnter unmountOnExit>
{(state) => (
<div className={`transition-opacity ${state}`}>
<div className="absolute top-0 left-0 w-full h-full bg-black bg-opacity-10 z-10"></div>
<div className="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-10"></div>
<div ref={ref} className="absolute z-20 ml-2 mt-3.5">
<div className="relative">
<svg
@@ -62,7 +62,7 @@ function Menu() {
</div>
<div className="space-y-1 text-center">
<p className="text-sm">{user?.name}</p>
<p className="text-[#77828C] text-xs">{user?.username}</p>
<p className="text-[#77828C] text-xs">{user?.role}</p>
</div>
</div>
<div className="border-b border-[#DAE0E5] py-3 space-y-2">
+86 -38
View File
@@ -28,6 +28,7 @@ function Schedule({ selectedDay, slots, events }: Props) {
const [email, setEmail] = useState<string>("");
const [name, setName] = useState<string>("");
const [phone, setPhone] = useState<string>("");
const [dateForInstantStart, setDateForInstantStart] = useState<Date>();
function handleChangeSlot(slot: number) {
setSlot(slot);
@@ -39,6 +40,7 @@ function Schedule({ selectedDay, slots, events }: Props) {
function handleChangeStartAt(startAt: Date) {
setStartAt(setDate(startAt, getDate(selectedDay)));
setDateForInstantStart(undefined);
}
function handleChangeDuration(duration: number) {
@@ -46,6 +48,10 @@ function Schedule({ selectedDay, slots, events }: Props) {
}
async function handleClickSave() {
console.log("slot", slot);
console.log("startAt", startAt);
console.log("duration", duration);
if (!slot || !startAt || !duration) return;
await addSchesuledSession();
@@ -70,7 +76,7 @@ function Schedule({ selectedDay, slots, events }: Props) {
// setIsLoading(true);
try {
await api
const result: any = await api
.post(`scheduled_sessions`, {
json: {
companyId: company!.id,
@@ -86,8 +92,12 @@ function Schedule({ selectedDay, slots, events }: Props) {
},
})
.json();
if (!dateForInstantStart) return;
window.open(`${result.url}?admin=true`);
} catch (error) {
alert((error as Error).message);
console.log(error);
}
// setIsLoading(false);
@@ -106,6 +116,29 @@ function Schedule({ selectedDay, slots, events }: Props) {
console.log("events", events);
}, [events]);
useEffect(() => {
if (!dateForInstantStart) return;
setSlot(1);
setStartAt(dateForInstantStart);
setDuration(30);
setDraftMode(true);
}, [dateForInstantStart]);
useEffect(() => {
function handleEscKey(e: KeyboardEvent) {
if (!draftMode && e.key !== "Escape") return;
setDraftMode(false);
}
document.addEventListener("keyup", handleEscKey, false);
return () => {
document.removeEventListener("keyup", handleEscKey, false);
};
}, []);
return (
<div className="relative h-screen overflow-y-auto bg-[#F2F2F2] text-sm">
<div className="fixed z-10 flex">
@@ -157,10 +190,14 @@ function Schedule({ selectedDay, slots, events }: Props) {
</div>
{draftMode && startAt && (
<div className="fixed top-0 right-0 flex flex-col justify-between h-screen overflow-y-auto bg-white shadow w-[320px]">
<div className="fixed top-0 right-0 flex flex-col justify-between h-screen overflow-y-auto bg-white shadow w-[320px] z-20">
<div className="space-y-4">
<div className="p-2 pl-4 flex items-center justify-between border-b border-[#DAE0E5]">
<p className="font-semibold">Запланировать демонстрацию</p>
<p className="font-semibold">
{dateForInstantStart
? "Начать демонстрацию"
: "Запланировать демонстрацию"}
</p>
<Button
color="tertiary"
icon={<CloseIcon />}
@@ -199,45 +236,47 @@ function Schedule({ selectedDay, slots, events }: Props) {
</div>
</div>
</div>
<div className="px-4 space-y-2">
<p className="font-semibold">Клиент</p>
<div className="space-y-4">
<div className="space-y-1">
<Label value="Email" />
<Input
type="email"
className="w-full h-10"
value={email}
handleChange={(value) => setEmail(value)}
/>
<span className="text-[#77828C] text-xs">
На указанный почтовый адрес придет необходимая для
подключения информация
</span>
</div>
<div className="space-y-1">
<Label value="Телефон" />
<Input
type="tel"
className="w-full h-10"
value={phone}
handleChange={(value) => setPhone(value)}
/>
</div>
<div className="space-y-1">
<Label value="Имя" />
<Input
className="w-full h-10"
value={name}
handleChange={(value) => setName(value)}
/>
{!dateForInstantStart && (
<div className="px-4 space-y-2">
<p className="font-semibold">Клиент</p>
<div className="space-y-4">
<div className="space-y-1">
<Label value="Email" />
<Input
type="email"
className="w-full h-10"
value={email}
handleChange={(value) => setEmail(value)}
/>
<span className="text-[#77828C] text-xs">
На указанный почтовый адрес придет необходимая для
подключения информация
</span>
</div>
<div className="space-y-1">
<Label value="Телефон" />
<Input
type="tel"
className="w-full h-10"
value={phone}
handleChange={(value) => setPhone(value)}
/>
</div>
<div className="space-y-1">
<Label value="Имя" />
<Input
className="w-full h-10"
value={name}
handleChange={(value) => setName(value)}
/>
</div>
</div>
</div>
</div>
)}
</div>
<div className="flex gap-2 p-4">
<Button type="submit" handleClick={handleClickSave}>
Запланировать
{dateForInstantStart ? "Начать" : "Запланировать"}
</Button>
<Button
type="button"
@@ -249,6 +288,15 @@ function Schedule({ selectedDay, slots, events }: Props) {
</div>
</div>
)}
<div className="fixed top-28 right-[336px] z-10">
<Button
disabled={draftMode}
handleClick={() => setDateForInstantStart(new Date())}
>
Начать демонстрацию с мгновенным запуском
</Button>
</div>
</div>
);
}
+3 -1
View File
@@ -191,7 +191,9 @@ function DashboardPage() {
</div>
<div className="flex justify-between items-center px-4 py-2 h-12 border-r border-b border-[#DAE0E5]">
<p className="text-sm font-semibold">Расписание</p>
<div className="flex items-center gap-4">
<p className="text-sm font-semibold">Расписание</p>
</div>
<div className="flex items-center gap-4">
<p className="text-sm font-semibold">
{format(selectedDay, "PPPP", { locale: ru })}
+3 -3
View File
@@ -5,11 +5,11 @@ import { useEffect } from "react";
import useAuthStore from "../stores/useAuthStore";
function HomePage() {
const accessToken = useAuthStore().accessToken;
const { user } = useAuthStore();
const navigate = useNavigate();
useEffect(() => {
if (accessToken) {
if (user?.accessToken) {
navigate("/dashboard");
} else {
navigate("/login");
@@ -17,7 +17,7 @@ function HomePage() {
}, []);
return (
<div className="p-8 flex flex-col gap-4 w-fit">
<div className="flex flex-col gap-4 p-8 w-fit">
<p>HomePage</p>
<Link to="/login">
<Button type="button">Login</Button>
+39 -40
View File
@@ -1,62 +1,61 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useEffect, useState } from "react";
import ky from "ky";
// import ReCAPTCHA from "react-google-recaptcha";
import { useState } from "react";
import Form from "../components/Form";
import Label from "../components/Label";
import Input from "../components/Input";
import Button from "../components/Button";
import { Link, useNavigate } from "react-router-dom";
import useAuthStore from "../stores/useAuthStore";
import toast from "react-hot-toast";
import IError from "../types/IError";
import IUser from "../types/IUser";
import api from "../utils/api";
function LoginPage() {
const [username, setUsername] = useState<string>();
const [password, setPassword] = useState<string>();
// const recaptchaRef = useRef(null);
const [accessToken, setAccessToken, setUser] = useAuthStore((state) => [
state.accessToken,
state.setAccessToken,
state.setUser,
]);
const [isLoading, setisLoading] = useState<boolean>(false);
const [loading, setLoading] = useState(false);
const { setUser } = useAuthStore();
const navigate = useNavigate();
async function login() {
setisLoading(true);
setLoading(true);
const result: any = await ky
.post(import.meta.env.VITE_API_URL + "/login", {
json: {
username,
password,
},
})
.json();
try {
const result: IUser | IError = await api
.post("login", {
credentials: "include",
json: { username, password },
})
.json();
if (result.error) {
alert(result.error);
return;
if ("error" in result) {
setLoading(false);
toast.error(`Error: ${result.error}`);
return;
}
setUser({
id: result.id,
username: "username",
accessToken: result.accessToken,
companyId: result.companyId,
name: result.name,
role: result.role,
buildIds: result.buildIds,
});
navigate("/dashboard");
} catch (error) {
setLoading(false);
toast.error((error as Error).message);
}
setAccessToken(result.accessToken);
setUser(result.user);
navigate("/dashboard");
setTimeout(() => {
setisLoading(false);
}, 3000);
}
useEffect(() => {
if (accessToken) {
navigate("/dashboard");
}
}, []);
return (
<div className="min-h-screen flex flex-col justify-center items-center p-8 flex-1 ">
<div className=" bg-white rounded-lg shadow-md overflow-hidden">
<div className="flex flex-col items-center justify-center flex-1 min-h-screen p-8 ">
<div className="overflow-hidden bg-white rounded-lg shadow-md ">
<div className="flex flex-col gap-6 p-12">
<p className="text-2xl font-semibold">Вход</p>
<Form handleSubmit={login}>
@@ -88,12 +87,12 @@ function LoginPage() {
type="submit"
size="medium"
className="mt-10"
loading={isLoading}
loading={loading}
>
Войти
</Button>
</Form>
<div className="self-center flex gap-6">
<div className="flex self-center gap-6">
<Link to="/registration" className="text-xs text-[#49A1F5]">
Нет аккаунта?
</Link>
+2 -5
View File
@@ -1,10 +1,9 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import IUser from "../types/IUser";
import { persist } from "zustand/middleware";
interface AuthState {
accessToken: string | null;
setAccessToken: (accessToken: string | null) => void;
user: IUser | null;
setUser: (user: IUser | null) => void;
}
@@ -12,8 +11,6 @@ interface AuthState {
const useAuthStore = create<AuthState>()(
persist(
(set) => ({
accessToken: null,
setAccessToken: (accessToken) => set({ accessToken }),
user: null,
setUser: (user) => set({ user }),
}),
+2 -1
View File
@@ -1,9 +1,10 @@
interface IUser {
id: string;
username: string;
accessToken: string;
companyId: string;
avatar?: string;
name: string;
username: string;
role: string;
buildIds?: string[];
}
+25 -2
View File
@@ -1,10 +1,33 @@
/* eslint-disable no-empty */
/* eslint-disable react-hooks/exhaustive-deps */
import { Navigate, Outlet } from "react-router-dom";
import api from "./api";
import { useEffect, useRef } from "react";
import useAuthStore from "../stores/useAuthStore";
function ProtectedRoute() {
const accessToken = useAuthStore((state) => state.accessToken);
const timeout = useRef<NodeJS.Timeout>();
const { user } = useAuthStore();
return accessToken ? <Outlet /> : <Navigate to="/login" />;
async function checkAuth() {
try {
await api.get("check").json();
} catch (error) {}
timeout.current = setTimeout(async () => {
await checkAuth();
}, 10000);
}
useEffect(() => {
checkAuth();
return () => {
clearTimeout(timeout.current);
};
}, []);
return user?.accessToken ? <Outlet /> : <Navigate to={"/login"} />;
}
export default ProtectedRoute;
+34 -2
View File
@@ -1,16 +1,48 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ky from "ky";
import useAuthStore from "../stores/useAuthStore";
import IError from "../types/IError";
import toast from "react-hot-toast";
const api = ky.extend({
prefixUrl: `${import.meta.env.VITE_API_URL}`,
prefixUrl: import.meta.env.VITE_API_URL,
hooks: {
beforeRequest: [
(request) => {
const accessToken = useAuthStore.getState().accessToken;
const accessToken = useAuthStore.getState().user?.accessToken;
request.headers.set("Authorization", `Bearer ${accessToken}`);
},
],
afterResponse: [
async (_, __, response) => {
if (response.status === 200) return;
if (response.status === 401) {
try {
const result: { accessToken: string } | IError = await ky
.post(`${import.meta.env.VITE_API_URL}/refresh`, {
credentials: "include",
})
.json();
if ("error" in result) {
window.location.href = "/login";
return;
}
useAuthStore.setState((prev) => ({
...prev,
user: { ...prev.user!, accessToken: result.accessToken },
}));
} catch (error) {
window.location.href = "/login";
}
} else {
toast.error(response.statusText);
}
},
],
},
});