lint and format

This commit is contained in:
2024-08-11 15:28:53 +02:00
parent da0cccfb0e
commit 57c5998435
36 changed files with 407 additions and 397 deletions

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

View File

@@ -9,5 +9,10 @@
"recommended": true
}
},
"javascript": {
"formatter": {
"semicolons": "asNeeded"
}
},
"formatter": { "indentStyle": "space", "indentWidth": 2 }
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,31 +1,31 @@
import type { FC, ReactNode } from "react";
import { useEffect } from "react";
import type { FC, ReactNode } from "react"
import { useEffect } from "react"
import { useRouter } from "next/router";
import { useRouter } from "next/router"
import { Loader } from "../loader";
import { Loader } from "../loader"
import { useMeQuery } from "@/services";
import { useMeQuery } from "@/services"
export const AuthRedirect: FC<{ children: ReactNode }> = ({ children }) => {
const router = useRouter();
const { data: user, isLoading } = useMeQuery();
const router = useRouter()
const { data: user, isLoading } = useMeQuery()
const isAuthPage =
router.pathname === "/login" || router.pathname === "/sign-up";
router.pathname === "/login" || router.pathname === "/sign-up"
useEffect(() => {
if (!isLoading && !user && !isAuthPage) {
router.push("/login");
router.push("/login")
}
}, [user, isLoading, isAuthPage, router]);
}, [user, isLoading, isAuthPage, router])
if (isLoading || (!user && !isAuthPage)) {
return (
<div className={"h-screen"}>
<Loader />
</div>
);
)
}
return <>{children}</>;
};
return <>{children}</>
}

View File

@@ -1,10 +1,10 @@
import type { ComponentPropsWithoutRef, FC } from "react";
import type { ComponentPropsWithoutRef, FC } from "react"
import { cn } from "@/helpers";
import { cn } from "@/helpers"
type Props = ComponentPropsWithoutRef<"button"> & {
variant?: "primary" | "outlined" | "icon";
};
variant?: "primary" | "outlined" | "icon"
}
export const Button: FC<Props> = ({
className,
@@ -18,9 +18,9 @@ export const Button: FC<Props> = ({
variant === "outlined" && "border-sky-700 bg-inherit text-sky-700",
variant === "icon" &&
"h-6 w-6 shrink-0 border-none bg-inherit p-0 text-sky-700 hover:bg-slate-100 hover:text-sky-800",
className
className,
)}
{...rest}
/>
);
};
)
}

View File

@@ -1,9 +1,9 @@
import { Loader } from "../loader";
import { Loader } from "../loader"
export const FullscreenLoader = () => {
return (
<div className={"flex h-screen items-center justify-center"}>
<Loader />
</div>
);
};
)
}

View File

@@ -1,6 +1,6 @@
export * from "./todolist";
export * from "./auth-redirect";
export * from "./button";
export * from "./input";
export * from "./loader";
export * from "./fullscreen-loader";
export * from "./todolist"
export * from "./auth-redirect"
export * from "./button"
export * from "./input"
export * from "./loader"
export * from "./fullscreen-loader"

View File

@@ -1,9 +1,9 @@
import type { DetailedHTMLProps, FC, InputHTMLAttributes } from "react";
import type { DetailedHTMLProps, FC, InputHTMLAttributes } from "react"
type Props = DetailedHTMLProps<
InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
>;
>
export const Input: FC<Props> = ({ className, ...rest }) => {
return (
@@ -11,5 +11,5 @@ export const Input: FC<Props> = ({ className, ...rest }) => {
className={`w-full rounded-md border border-gray-300 px-4 py-2 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-600 ${className}`}
{...rest}
/>
);
};
)
}

View File

@@ -3,5 +3,5 @@ export const Loader = () => {
<div className={"flex h-full w-full items-center justify-center"}>
<span className="loader" />
</div>
);
};
)
}

View File

@@ -1,19 +1,19 @@
import type { ChangeEvent, FC, FormEvent, MouseEventHandler } from "react";
import { memo, useState } from "react";
import type { ChangeEvent, FC, FormEvent } from "react"
import { memo, useState } from "react"
import { Trash, Plus } from "lucide-react";
import { Plus, Trash } from "lucide-react"
import type {
Task,
Todolist as TodolistType,
} from "../../services/todolist-api/todolists";
import { Button } from "../button";
import { Input } from "../input";
} from "../../services/todolist-api/todolists"
import { Button } from "../button"
import { Input } from "../input"
import {
ToggleGroup,
ToggleGroupItem,
} from "@/components/toggle-group/toggle-group";
} from "@/components/toggle-group/toggle-group"
import {
TaskStatus,
useCreateTaskMutation,
@@ -21,62 +21,62 @@ import {
useDeleteTodolistMutation,
useGetTasksQuery,
useUpdateTaskMutation,
} from "@/services";
} from "@/services"
type Props = {
todolist: TodolistType;
};
todolist: TodolistType
}
type Filter = "all" | "active" | "completed";
type Filter = "all" | "active" | "completed"
export const Todolist: FC<Props> = memo(({ todolist }) => {
const { data: tasks, isLoading } = useGetTasksQuery(todolist.id);
const { mutate: putTask } = useUpdateTaskMutation();
const { mutate: deleteTask } = useDeleteTaskMutation();
const { mutate: deleteTodolist } = useDeleteTodolistMutation();
const { mutate: createTask } = useCreateTaskMutation();
const [newTaskTitle, setNewTaskTitle] = useState("");
const [filter, setFilter] = useState("all");
const { data: tasks, isLoading } = useGetTasksQuery(todolist.id)
const { mutate: putTask } = useUpdateTaskMutation()
const { mutate: deleteTask } = useDeleteTaskMutation()
const { mutate: deleteTodolist } = useDeleteTodolistMutation()
const { mutate: createTask } = useCreateTaskMutation()
const [newTaskTitle, setNewTaskTitle] = useState("")
const [filter, setFilter] = useState("all")
const handleChangeStatus = (todolistId: string, task: Task) => {
const newTask: Task = {
...task,
status:
task.status === TaskStatus.DONE ? TaskStatus.OPEN : TaskStatus.DONE,
};
}
putTask({ todolistId, task: newTask });
};
putTask({ todolistId, task: newTask })
}
const handleDeleteTask = (todolistId: string, taskId: string) => {
deleteTask({ todolistId, taskId });
};
deleteTask({ todolistId, taskId })
}
const handleDeleteTodolist = (todolistId: string) => {
deleteTodolist({ todolistId });
};
deleteTodolist({ todolistId })
}
const handleAddTask = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
createTask({ todolistId: todolist.id, title: newTaskTitle });
setNewTaskTitle("");
};
e.preventDefault()
createTask({ todolistId: todolist.id, title: newTaskTitle })
setNewTaskTitle("")
}
const handleNewTaskTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
setNewTaskTitle(e.target.value);
};
setNewTaskTitle(e.target.value)
}
const handleFilterChange = (value: string) => {
setFilter(value as Filter);
};
setFilter(value as Filter)
}
if (isLoading) return <div>loading...</div>;
if (isLoading) return <div>loading...</div>
const filteredTasks = tasks?.filter((task) => {
switch (filter) {
case "active":
return task.status === TaskStatus.OPEN;
return task.status === TaskStatus.OPEN
case "completed":
return task.status === TaskStatus.DONE;
return task.status === TaskStatus.DONE
default:
return true;
return true
}
});
})
return (
<div
@@ -122,7 +122,7 @@ export const Todolist: FC<Props> = memo(({ todolist }) => {
<Trash size={16} />
</Button>
</div>
);
)
})}
<div className={"flex"}>
<ToggleGroup
@@ -143,5 +143,5 @@ export const Todolist: FC<Props> = memo(({ todolist }) => {
</ToggleGroup>
</div>
</div>
);
});
)
})

View File

@@ -1,12 +1,12 @@
"use client";
"use client"
import * as React from "react";
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
import { cn } from "@/helpers";
import { cn } from "@/helpers"
const toggleVariants = cva(
"inline-flex border border-sky-700 items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-sky-700 data-[state=on]:text-white",
@@ -27,14 +27,14 @@ const toggleVariants = cva(
variant: "default",
size: "default",
},
}
);
},
)
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
});
})
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
@@ -50,16 +50,16 @@ const ToggleGroup = React.forwardRef<
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
))
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
@@ -69,15 +69,15 @@ const ToggleGroupItem = React.forwardRef<
variant: context.variant || variant,
size: context.size || size,
}),
className
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem };
export { ToggleGroup, ToggleGroupItem }

View File

@@ -1,3 +1,3 @@
export * from "./routes";
export * from "./query-keys";
export * from "./result-code";
export * from "./routes"
export * from "./query-keys"
export * from "./result-code"

View File

@@ -2,4 +2,4 @@ export const QUERY_KEYS = {
TODOLISTS: "todolists",
TASKS: "tasks",
ME: "me",
} as const;
} as const

View File

@@ -1,4 +1,4 @@
export const ROUTES = {
HOME: "/",
LOGIN: "/login",
} as const;
} as const

26
src/env/client.mjs vendored
View File

@@ -1,35 +1,35 @@
// @ts-check
import { clientEnv, clientSchema } from "./schema.mjs";
import { clientEnv, clientSchema } from "./schema.mjs"
const _clientEnv = clientSchema.safeParse(clientEnv);
const _clientEnv = clientSchema.safeParse(clientEnv)
export const formatErrors = (
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
errors
errors,
) =>
Object.entries(errors)
.map(([name, value]) => {
if (value && "_errors" in value)
return `${name}: ${value._errors.join(", ")}\n`;
return `${name}: ${value._errors.join(", ")}\n`
})
.filter(Boolean);
.filter(Boolean)
if (!_clientEnv.success) {
console.error(
"❌ Invalid environment variables:\n",
...formatErrors(_clientEnv.error.format())
);
throw new Error("Invalid environment variables");
...formatErrors(_clientEnv.error.format()),
)
throw new Error("Invalid environment variables")
}
for (let key of Object.keys(_clientEnv.data)) {
for (const key of Object.keys(_clientEnv.data)) {
if (!key.startsWith("NEXT_PUBLIC_")) {
console.warn(
`❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`
);
`❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`,
)
throw new Error("Invalid public environment variable name");
throw new Error("Invalid public environment variable name")
}
}
export const env = _clientEnv.data;
export const env = _clientEnv.data

8
src/env/schema.mjs vendored
View File

@@ -1,5 +1,5 @@
// @ts-check
import { z } from "zod";
import { z } from "zod"
/**
* Specify your server-side environment variables schema here.
@@ -7,7 +7,7 @@ import { z } from "zod";
*/
export const serverSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]),
});
})
/**
* Specify your client-side environment variables schema here.
@@ -16,7 +16,7 @@ export const serverSchema = z.object({
*/
export const clientSchema = z.object({
// NEXT_PUBLIC_CLIENTVAR: z.string(),
});
})
/**
* You can't destruct `process.env` as a regular object, so you have to do
@@ -26,4 +26,4 @@ export const clientSchema = z.object({
*/
export const clientEnv = {
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
};
}

20
src/env/server.mjs vendored
View File

@@ -3,25 +3,25 @@
* This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars.
* It has to be a `.mjs`-file to be imported there.
*/
import { serverSchema } from "./schema.mjs";
import { env as clientEnv, formatErrors } from "./client.mjs";
import { serverSchema } from "./schema.mjs"
import { env as clientEnv, formatErrors } from "./client.mjs"
const _serverEnv = serverSchema.safeParse(process.env);
const _serverEnv = serverSchema.safeParse(process.env)
if (!_serverEnv.success) {
console.error(
"❌ Invalid environment variables:\n",
...formatErrors(_serverEnv.error.format())
);
throw new Error("Invalid environment variables");
...formatErrors(_serverEnv.error.format()),
)
throw new Error("Invalid environment variables")
}
for (let key of Object.keys(_serverEnv.data)) {
for (const key of Object.keys(_serverEnv.data)) {
if (key.startsWith("NEXT_PUBLIC_")) {
console.warn("❌ You are exposing a server-side env-variable:", key);
console.warn("❌ You are exposing a server-side env-variable:", key)
throw new Error("You are exposing a server-side env-variable");
throw new Error("You are exposing a server-side env-variable")
}
}
export const env = { ..._serverEnv.data, ...clientEnv };
export const env = { ..._serverEnv.data, ...clientEnv }

View File

@@ -1,17 +1,17 @@
import type { ResultCode } from "@/constants";
import type { ResultCode } from "@/constants"
type ApiResponseSuccess<T> = {
data: T;
messages?: never;
fieldsErrors?: never;
resultCode: ResultCode.Success;
};
data: T
messages?: never
fieldsErrors?: never
resultCode: ResultCode.Success
}
type ApiResponseError<T> = {
data: T;
messages: string[];
fieldsErrors?: string[];
resultCode: ResultCode.InvalidRequest | ResultCode.InvalidRequestWithCaptcha;
};
data: T
messages: string[]
fieldsErrors?: string[]
resultCode: ResultCode.InvalidRequest | ResultCode.InvalidRequestWithCaptcha
}
export type ApiResponse<T> = ApiResponseSuccess<T> | ApiResponseError<T>;
export type ApiResponse<T> = ApiResponseSuccess<T> | ApiResponseError<T>

View File

@@ -1,6 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs))
}

View File

@@ -1 +1 @@
export * from "./classnames";
export * from "./classnames"

View File

@@ -1,4 +1,4 @@
import type { ApiResponse } from "@/helpers";
import type { ApiResponse } from "@/helpers"
export const handleError = <T>(data: ApiResponse<T>): ApiResponse<T> => {
// if (data.resultCode !== 0) {
@@ -7,5 +7,5 @@ export const handleError = <T>(data: ApiResponse<T>): ApiResponse<T> => {
// throw new Error(error);
// }
return data;
};
return data
}

View File

@@ -1,4 +1,4 @@
export * from "./handle-error";
export * from "./no-refetch";
export * from "./api-response";
export * from "./classnames";
export * from "./handle-error"
export * from "./no-refetch"
export * from "./api-response"
export * from "./classnames"

View File

@@ -5,4 +5,4 @@ export const noRefetch = {
refetchOnMount: false,
refetchIntervalInBackground: false,
retry: false,
} as const;
} as const

View File

@@ -1,11 +1,11 @@
import "../styles/globals.css";
import "../styles/globals.css"
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { type AppType } from "next/dist/shared/lib/utils";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query"
import type { AppType } from "next/dist/shared/lib/utils"
import { AuthRedirect } from "@/components";
import { AuthRedirect } from "@/components"
const queryClient = new QueryClient();
const queryClient = new QueryClient()
const MyApp: AppType = ({ Component, pageProps }) => {
return (
@@ -14,7 +14,7 @@ const MyApp: AppType = ({ Component, pageProps }) => {
<Component {...pageProps} />
</AuthRedirect>
</QueryClientProvider>
);
};
)
}
export default MyApp;
export default MyApp

View File

@@ -1,39 +1,38 @@
import type { ChangeEvent, FormEvent } from "react";
import { useState } from "react";
import type { ChangeEvent, FormEvent } from "react"
import { useState } from "react"
import { type NextPage } from "next";
import Head from "next/head";
import type { NextPage } from "next"
import Head from "next/head"
import { Todolist, Button, FullscreenLoader, Input } from "@/components";
import { Todolist, Button, FullscreenLoader, Input } from "@/components"
import {
useCreateTodolistMutation,
useLogoutMutation,
useTodolistsQuery,
} from "@/services";
} from "@/services"
const Home: NextPage = () => {
const [newTodolistTitle, setNewTodolistTitle] = useState("");
const { mutate: logout } = useLogoutMutation();
const { data: todolists, isLoading: isTodolistsLoading } =
useTodolistsQuery();
const [newTodolistTitle, setNewTodolistTitle] = useState("")
const { mutate: logout } = useLogoutMutation()
const { data: todolists, isLoading: isTodolistsLoading } = useTodolistsQuery()
const handleLogout = () => {
logout();
};
logout()
}
const { mutate: createTodolist } = useCreateTodolistMutation();
const { mutate: createTodolist } = useCreateTodolistMutation()
const handleAddTodolist = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
createTodolist({ title: newTodolistTitle });
setNewTodolistTitle("");
};
e.preventDefault()
createTodolist({ title: newTodolistTitle })
setNewTodolistTitle("")
}
const handleNewTodolistTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
setNewTodolistTitle(e.target.value);
};
setNewTodolistTitle(e.target.value)
}
if (isTodolistsLoading) return <FullscreenLoader />;
if (isTodolistsLoading) return <FullscreenLoader />
return (
<>
@@ -59,12 +58,12 @@ const Home: NextPage = () => {
</form>
<div className={"flex flex-wrap gap-4"}>
{todolists?.map((todolist) => {
return <Todolist todolist={todolist} key={todolist.id} />;
return <Todolist todolist={todolist} key={todolist.id} />
})}
</div>
</main>
</>
);
};
)
}
export default Home;
export default Home

View File

@@ -1,37 +1,37 @@
import type { ChangeEvent } from "react";
import React, { useState } from "react";
import type { ChangeEvent } from "react"
import React, { useState } from "react"
import type { NextPage } from "next";
import type { NextPage } from "next"
import { Button, Input } from "@/components";
import { useLoginMutation } from "@/services";
import { Button, Input } from "@/components"
import { useLoginMutation } from "@/services"
const Login: NextPage = () => {
const { mutate: login } = useLoginMutation();
const { mutate: login } = useLoginMutation()
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [remember, setRemember] = useState(true);
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [remember, setRemember] = useState(true)
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
setPassword(e.target.value);
};
setPassword(e.target.value)
}
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};
setEmail(e.target.value)
}
const handleRememberChange = (e: ChangeEvent<HTMLInputElement>) => {
setRemember(e.target.checked);
};
setRemember(e.target.checked)
}
const handleSubmit = () => {
login({
email,
password,
rememberMe: remember,
});
};
})
}
return (
<div className={"flex h-screen items-center justify-center"}>
@@ -63,7 +63,7 @@ const Login: NextPage = () => {
</Button>
</div>
</div>
);
};
)
}
export default Login;
export default Login

View File

@@ -1,21 +1,21 @@
import type { FormEvent } from "react";
import React from "react";
import type { FormEvent } from "react"
import React from "react"
import type { NextPage } from "next";
import type { NextPage } from "next"
import { Button, Input } from "@/components";
import { useSignUpMutation } from "@/services";
import { Button, Input } from "@/components"
import { useSignUpMutation } from "@/services"
const Login: NextPage = () => {
const { mutate: signUp } = useSignUpMutation();
const { mutate: signUp } = useSignUpMutation()
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
e.preventDefault()
const formData = new FormData(e.currentTarget)
const values = Object.fromEntries(formData) as any;
signUp(values);
};
const values = Object.fromEntries(formData) as any
signUp(values)
}
return (
<div className={"flex h-screen items-center justify-center"}>
@@ -43,7 +43,7 @@ const Login: NextPage = () => {
</Button>
</form>
</div>
);
};
)
}
export default Login;
export default Login

View File

@@ -1,2 +1,2 @@
export * from "./todolist-api/auth";
export * from "./todolist-api/todolists";
export * from "./todolist-api/auth"
export * from "./todolist-api/todolists"

View File

@@ -4,37 +4,37 @@ import type {
MeResponse,
PostLoginArgs,
PostSignUpArgs,
} from "../todolists";
} from "../todolists"
import { handleError } from "@/helpers";
import { todolistApiInstance } from "@/services/todolist-api/todolist-api.instance";
import { handleError } from "@/helpers"
import { todolistApiInstance } from "@/services/todolist-api/todolist-api.instance"
export const AuthApi = {
async login(args: PostLoginArgs) {
const res = await todolistApiInstance.post<LoginResponse>(
"/auth/login",
args,
);
)
localStorage.setItem("accessToken", res.data.accessToken);
localStorage.setItem("refreshToken", res.data.refreshToken);
localStorage.setItem("accessToken", res.data.accessToken)
localStorage.setItem("refreshToken", res.data.refreshToken)
return res.data;
return res.data
},
async signUp(args: PostSignUpArgs) {
const res = await todolistApiInstance.post<any>("/auth/sign-up", args);
const res = await todolistApiInstance.post<any>("/auth/sign-up", args)
return res.data;
return res.data
},
async logout() {
const res = await todolistApiInstance.delete<LogoutResponse>("/auth/login");
const res = await todolistApiInstance.delete<LogoutResponse>("/auth/login")
return handleError(res.data);
return handleError(res.data)
},
async me() {
const res = await todolistApiInstance.get<MeResponse>("/auth/me");
const res = await todolistApiInstance.get<MeResponse>("/auth/me")
return handleError(res.data);
return handleError(res.data)
},
};
}

View File

@@ -1,46 +1,46 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "next/router";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useRouter } from "next/router"
import { QUERY_KEYS, ROUTES } from "@/constants";
import { noRefetch } from "@/helpers";
import { AuthApi } from "@/services/todolist-api/auth/auth.api";
import { QUERY_KEYS, ROUTES } from "@/constants"
import { noRefetch } from "@/helpers"
import { AuthApi } from "@/services/todolist-api/auth/auth.api"
export const useMeQuery = () => {
return useQuery({
queryFn: AuthApi.me,
queryKey: [QUERY_KEYS.ME],
...noRefetch,
});
};
})
}
export const useLoginMutation = () => {
const router = useRouter();
const queryClient = useQueryClient();
const router = useRouter()
const queryClient = useQueryClient()
return useMutation({
mutationFn: AuthApi.login,
onSuccess: async () => {
await queryClient.invalidateQueries([QUERY_KEYS.ME]);
await router.push(ROUTES.HOME);
await queryClient.invalidateQueries([QUERY_KEYS.ME])
await router.push(ROUTES.HOME)
},
});
};
})
}
export const useLogoutMutation = () => {
const queryClient = useQueryClient();
const router = useRouter();
const queryClient = useQueryClient()
const router = useRouter()
return useMutation({
mutationFn: AuthApi.logout,
onSuccess: async () => {
await queryClient.invalidateQueries([QUERY_KEYS.ME]);
await router.push(ROUTES.LOGIN);
await queryClient.invalidateQueries([QUERY_KEYS.ME])
await router.push(ROUTES.LOGIN)
},
});
};
})
}
export const useSignUpMutation = () => {
return useMutation({
mutationFn: AuthApi.signUp,
});
};
})
}

View File

@@ -1,2 +1,2 @@
export * from "./auth.api";
export * from "./auth.hooks";
export * from "./auth.api"
export * from "./auth.hooks"

View File

@@ -1,19 +1,19 @@
import { Mutex } from "async-mutex";
import axios from "axios";
import router from "next/router";
import { Mutex } from "async-mutex"
import axios from "axios"
import router from "next/router"
const mutex = new Mutex();
const mutex = new Mutex()
let refreshedAt: number | null = null;
let refreshedAt: number | null = null
export const todolistApiInstance = axios.create({
baseURL: "http://localhost:3000",
});
})
async function refreshAccessToken(): Promise<string> {
const res = await todolistApiInstance.post<{
refreshToken: string;
accessToken: string;
refreshToken: string
accessToken: string
}>(
"/auth/refresh-token",
{},
@@ -22,76 +22,76 @@ async function refreshAccessToken(): Promise<string> {
Authorization: `Bearer ${localStorage.getItem("refreshToken")}`,
},
},
);
)
localStorage.setItem("accessToken", res.data.accessToken);
localStorage.setItem("refreshToken", res.data.refreshToken);
localStorage.setItem("accessToken", res.data.accessToken)
localStorage.setItem("refreshToken", res.data.refreshToken)
return res.data.accessToken;
return res.data.accessToken
}
todolistApiInstance.interceptors.request.use(
async (config) => {
if (!config?.url?.includes("refresh-token")) {
await mutex.waitForUnlock();
await mutex.waitForUnlock()
}
config.headers.Authorization =
config.headers.Authorization ??
`Bearer ${localStorage.getItem("accessToken")}`;
`Bearer ${localStorage.getItem("accessToken")}`
return config;
return config
},
(error) => Promise.reject(error),
);
)
todolistApiInstance.interceptors.response.use(
(response) => response,
async (error) => {
if (error?.response?.request?.responseURL?.includes("refresh-token")) {
return;
return
}
await mutex.waitForUnlock();
const originalRequest = error.config;
await mutex.waitForUnlock()
const originalRequest = error.config
// Check for a 401 response and if this request hasn't been retried yet
if (error.response?.status === 401 && !originalRequest._retry) {
if (!mutex.isLocked()) {
originalRequest._retry = true;
originalRequest._retry = true
// Use a mutex to ensure that token refresh logic is not run multiple times in parallel
const release = await mutex.acquire();
const release = await mutex.acquire()
if (refreshedAt && refreshedAt + 60000 > new Date().getTime()) {
// If the token has been refreshed within the last minute, use the refreshed token
originalRequest.headers.Authorization = `Bearer ${localStorage.getItem("accessToken")}`;
release();
originalRequest.headers.Authorization = `Bearer ${localStorage.getItem("accessToken")}`
release()
return todolistApiInstance(originalRequest);
return todolistApiInstance(originalRequest)
}
refreshedAt = new Date().getTime();
refreshedAt = new Date().getTime()
try {
const newAccessToken = await refreshAccessToken();
const newAccessToken = await refreshAccessToken()
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`
return todolistApiInstance(originalRequest); // Retry the original request with the new token
return todolistApiInstance(originalRequest) // Retry the original request with the new token
} catch (error) {
console.log(window.location.pathname);
console.log(window.location.pathname)
if (!["/login", "/sign-up"].includes(window.location.pathname)) {
router.push("/login");
router.push("/login")
}
} finally {
release();
release()
}
} else {
await mutex.waitForUnlock();
const newAccessToken = localStorage.getItem("accessToken");
await mutex.waitForUnlock()
const newAccessToken = localStorage.getItem("accessToken")
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`
return todolistApiInstance(originalRequest); // Retry the original request with the new token
return todolistApiInstance(originalRequest) // Retry the original request with the new token
}
}
return Promise.reject(error);
return Promise.reject(error)
},
);
)

View File

@@ -1,3 +1,3 @@
export * from "./todolists.hooks";
export * from "./todolists.api";
export * from "./todolists.types";
export * from "./todolists.hooks"
export * from "./todolists.api"
export * from "./todolists.types"

View File

@@ -1,4 +1,4 @@
import { handleError } from "@/helpers";
import { handleError } from "@/helpers"
import type {
CreateTaskResponse,
CreateTodolistResponse,
@@ -8,14 +8,14 @@ import type {
TasksResponse,
Todolist,
UpdateTaskResponse,
} from "@/services";
import { todolistApiInstance } from "@/services/todolist-api/todolist-api.instance";
} from "@/services"
import { todolistApiInstance } from "@/services/todolist-api/todolist-api.instance"
export const TodolistAPI = {
async getTodolists() {
const res = await todolistApiInstance.get<Todolist[]>("/todolists");
const res = await todolistApiInstance.get<Todolist[]>("/todolists")
return res.data;
return res.data
},
async createTodolist({ title }: { title: string }) {
@@ -23,64 +23,64 @@ export const TodolistAPI = {
"/todolists",
{
title,
}
);
},
)
return handleError(res.data);
return handleError(res.data)
},
async deleteTodolist({ todolistId }: { todolistId: string }) {
const res = await todolistApiInstance.delete<DeleteTodolistResponse>(
`/todolists/${todolistId}`
);
`/todolists/${todolistId}`,
)
return res.data;
return res.data
},
async getTodolistTasks({ todolistId }: { todolistId: string }) {
const res = await todolistApiInstance.get<TasksResponse>(
`/todolists/${todolistId}/tasks`
);
`/todolists/${todolistId}/tasks`,
)
return res.data;
return res.data
},
async createTask({
todolistId,
title,
}: {
todolistId: string;
title: string;
todolistId: string
title: string
}) {
const res = await todolistApiInstance.post<CreateTaskResponse>(
`/todolists/${todolistId}/tasks`,
{ title }
);
{ title },
)
return res.data;
return res.data
},
async updateTask({ todolistId, task }: { todolistId: string; task: Task }) {
const { id, ...rest } = task;
const { id, ...rest } = task
const res = await todolistApiInstance.patch<UpdateTaskResponse>(
`/todolists/${todolistId}/tasks/${id}`,
rest
);
rest,
)
return res.data;
return res.data
},
async deleteTask({
todolistId,
taskId,
}: {
todolistId: string;
taskId: string;
todolistId: string
taskId: string
}) {
const res = await todolistApiInstance.delete<DeleteTaskResponse>(
`/todolists/${todolistId}/tasks/${taskId}`
);
`/todolists/${todolistId}/tasks/${taskId}`,
)
return res.data;
return res.data
},
};
}

View File

@@ -1,80 +1,80 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { QUERY_KEYS } from "@/constants";
import { noRefetch } from "@/helpers";
import { TodolistAPI } from "@/services";
import { QUERY_KEYS } from "@/constants"
import { noRefetch } from "@/helpers"
import { TodolistAPI } from "@/services"
export const useTodolistsQuery = () => {
return useQuery({
queryFn: TodolistAPI.getTodolists,
queryKey: [QUERY_KEYS.TODOLISTS],
...noRefetch,
});
};
})
}
export const useCreateTodolistMutation = () => {
const queryClient = useQueryClient();
const queryClient = useQueryClient()
return useMutation({
mutationFn: TodolistAPI.createTodolist,
//todo: add onMutate
onSuccess: async () => {
await queryClient.invalidateQueries([QUERY_KEYS.TODOLISTS]);
await queryClient.invalidateQueries([QUERY_KEYS.TODOLISTS])
},
});
};
})
}
export const useDeleteTodolistMutation = () => {
const queryClient = useQueryClient();
const queryClient = useQueryClient()
return useMutation({
mutationFn: TodolistAPI.deleteTodolist,
onSuccess: () => {
queryClient.invalidateQueries([QUERY_KEYS.TODOLISTS]);
queryClient.invalidateQueries([QUERY_KEYS.TODOLISTS])
},
});
};
})
}
export const useGetTasksQuery = (todolistId: string) => {
return useQuery({
queryKey: [QUERY_KEYS.TASKS, todolistId],
queryFn: () => TodolistAPI.getTodolistTasks({ todolistId }),
});
};
})
}
export const useCreateTaskMutation = () => {
const queryClient = useQueryClient();
const queryClient = useQueryClient()
return useMutation({
mutationFn: TodolistAPI.createTask,
onSuccess: (res) => {
const todolistId = res.todoListId;
const todolistId = res.todoListId
queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId]);
queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId])
},
});
};
})
}
export const useUpdateTaskMutation = () => {
const queryClient = useQueryClient();
const queryClient = useQueryClient()
return useMutation({
mutationFn: TodolistAPI.updateTask,
onSuccess: async (_, { todolistId }) => {
await queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId]);
await queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId])
},
});
};
})
}
export const useDeleteTaskMutation = () => {
const queryClient = useQueryClient();
const queryClient = useQueryClient()
return useMutation({
mutationFn: TodolistAPI.deleteTask,
onSuccess: (_, variables) => {
const todolistId = variables.todolistId;
const todolistId = variables.todolistId
queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId]);
queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId])
},
});
};
})
}

View File

@@ -1,64 +1,64 @@
import type { ApiResponse } from "@/helpers";
import type { ApiResponse } from "@/helpers"
export type TasksResponse = Task[];
export type TasksResponse = Task[]
export type Task = {
order: string;
title: string;
description?: string | null;
deadline?: string | null;
status: TaskStatus;
priority: TaskPriority;
ownerId: string;
todoListId: string;
id: string;
createdAt: string;
updatedAt: string;
};
order: string
title: string
description?: string | null
deadline?: string | null
status: TaskStatus
priority: TaskPriority
ownerId: string
todoListId: string
id: string
createdAt: string
updatedAt: string
}
export type Todolist = {
id: string;
title: string;
addedDate: Date;
order: number;
};
id: string
title: string
addedDate: Date
order: number
}
export type PostLoginArgs = {
email: string;
password: string;
rememberMe: boolean;
};
email: string
password: string
rememberMe: boolean
}
export type PostSignUpArgs = {
email: string;
password: string;
username?: string;
};
email: string
password: string
username?: string
}
export type LoginResponseData = {
accessToken: string;
refreshToken: string;
};
accessToken: string
refreshToken: string
}
export type MeResponseData = {
id: number;
login: string;
email: string;
};
id: number
login: string
email: string
}
export type LoginResponse = LoginResponseData;
export type LogoutResponse = ApiResponse<never>;
export type MeResponse = ApiResponse<MeResponseData>;
export type LoginResponse = LoginResponseData
export type LogoutResponse = ApiResponse<never>
export type MeResponse = ApiResponse<MeResponseData>
export type DeleteTodolistResponse = ApiResponse<never>;
export type CreateTodolistResponse = ApiResponse<CreateTodolistResponseData>;
export type DeleteTodolistResponse = ApiResponse<never>
export type CreateTodolistResponse = ApiResponse<CreateTodolistResponseData>
export type CreateTodolistResponseData = {
item: Todolist;
};
item: Todolist
}
export type CreateTaskResponse = Task;
export type DeleteTaskResponse = undefined;
export type UpdateTaskResponse = undefined;
export type CreateTaskResponse = Task
export type DeleteTaskResponse = undefined
export type UpdateTaskResponse = undefined
export enum TaskStatus {
OPEN = "OPEN",