mirror of
https://github.com/ershisan99/todolist_next.git
synced 2025-12-16 12:33:57 +00:00
wip
This commit is contained in:
@@ -4,7 +4,11 @@
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended", "@it-incubator/eslint-config"],
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"@it-incubator/eslint-config"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-imports": "warn"
|
||||
}
|
||||
|
||||
@@ -6,14 +6,21 @@
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"lint": "next lint",
|
||||
"start": "next start"
|
||||
"start": "next start",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"axios": "^1.3.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.377.0",
|
||||
"next": "13.2.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
4049
pnpm-lock.yaml
generated
4049
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,7 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
module.exports = {
|
||||
plugins: [require.resolve("prettier-plugin-tailwindcss"), require.resolve("@it-incubator/prettier-config")],
|
||||
plugins: [
|
||||
require.resolve("prettier-plugin-tailwindcss"),
|
||||
require.resolve("@it-incubator/prettier-config"),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import type { ButtonHTMLAttributes, DetailedHTMLProps, FC } from "react";
|
||||
import type { ComponentPropsWithoutRef, FC } from "react";
|
||||
|
||||
type Props = DetailedHTMLProps<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
>;
|
||||
import { cn } from "@/helpers";
|
||||
|
||||
export const Button: FC<Props> = ({ className, ...rest }) => {
|
||||
type Props = ComponentPropsWithoutRef<"button"> & {
|
||||
variant?: "primary" | "outlined" | "icon";
|
||||
};
|
||||
|
||||
export const Button: FC<Props> = ({
|
||||
className,
|
||||
variant = "primary",
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
className={`rounded-md border border-gray-300 bg-sky-700 px-4 py-2 text-white focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-600 ${className}`}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-md border border-gray-300 bg-sky-700 px-4 py-2 text-white focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-600",
|
||||
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
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,11 +7,9 @@ type Props = DetailedHTMLProps<
|
||||
|
||||
export const Input: FC<Props> = ({ className, ...rest }) => {
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import type { ChangeEvent, FC, MouseEventHandler } from "react";
|
||||
import type { ChangeEvent, FC, FormEvent, MouseEventHandler } from "react";
|
||||
import { memo, useState } from "react";
|
||||
|
||||
import type { Task, Todolist as TodolistType } from "../../services/todolists";
|
||||
import { Trash, Plus } from "lucide-react";
|
||||
|
||||
import type {
|
||||
Task,
|
||||
Todolist as TodolistType,
|
||||
} from "../../services/todolist-api/todolists";
|
||||
import { Button } from "../button";
|
||||
import { Input } from "../input";
|
||||
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/components/toggle-group/toggle-group";
|
||||
import {
|
||||
TaskStatus,
|
||||
useCreateTaskMutation,
|
||||
useDeleteTaskMutation,
|
||||
useDeleteTodolistMutation,
|
||||
@@ -29,7 +39,11 @@ export const Todolist: FC<Props> = memo(({ todolist }) => {
|
||||
const [filter, setFilter] = useState("all");
|
||||
|
||||
const handleChangeStatus = (todolistId: string, task: Task) => {
|
||||
const newTask = { ...task, status: task.status === 0 ? 2 : 0 };
|
||||
const newTask: Task = {
|
||||
...task,
|
||||
status:
|
||||
task.status === TaskStatus.DONE ? TaskStatus.OPEN : TaskStatus.DONE,
|
||||
};
|
||||
|
||||
putTask({ todolistId, task: newTask });
|
||||
};
|
||||
@@ -39,7 +53,8 @@ export const Todolist: FC<Props> = memo(({ todolist }) => {
|
||||
const handleDeleteTodolist = (todolistId: string) => {
|
||||
deleteTodolist({ todolistId });
|
||||
};
|
||||
const handleAddTask = () => {
|
||||
const handleAddTask = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
createTask({ todolistId: todolist.id, title: newTaskTitle });
|
||||
setNewTaskTitle("");
|
||||
};
|
||||
@@ -47,17 +62,17 @@ export const Todolist: FC<Props> = memo(({ todolist }) => {
|
||||
setNewTaskTitle(e.target.value);
|
||||
};
|
||||
|
||||
const handleFilterChange: MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
setFilter(e.currentTarget.value as Filter);
|
||||
const handleFilterChange = (value: string) => {
|
||||
setFilter(value as Filter);
|
||||
};
|
||||
|
||||
if (isLoading) return <div>loading...</div>;
|
||||
const filteredTasks = tasks?.items?.filter((task) => {
|
||||
const filteredTasks = tasks?.filter((task) => {
|
||||
switch (filter) {
|
||||
case "active":
|
||||
return task.status === 0;
|
||||
return task.status === TaskStatus.OPEN;
|
||||
case "completed":
|
||||
return task.status === 2;
|
||||
return task.status === TaskStatus.DONE;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
@@ -67,40 +82,65 @@ export const Todolist: FC<Props> = memo(({ todolist }) => {
|
||||
<div
|
||||
key={todolist.id}
|
||||
className={
|
||||
"w- flex w-72 flex-col gap-3 rounded-md border border-gray-300 p-4"
|
||||
"flex w-72 flex-col gap-3 rounded-md border border-gray-300 p-4"
|
||||
}
|
||||
>
|
||||
<div className={"flex items-center justify-between "}>
|
||||
<h2 className={"text-xl"}>{todolist.title}</h2>
|
||||
<button onClick={() => handleDeleteTodolist(todolist.id)}>x</button>
|
||||
<Button
|
||||
variant={"icon"}
|
||||
onClick={() => handleDeleteTodolist(todolist.id)}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={"flex w-52 gap-4"}>
|
||||
<form
|
||||
onSubmit={handleAddTask}
|
||||
className={"relative flex w-full items-center gap-4"}
|
||||
>
|
||||
<Input value={newTaskTitle} onChange={handleNewTaskTitleChange} />
|
||||
<Button onClick={handleAddTask}>+</Button>
|
||||
</div>
|
||||
{filteredTasks?.map((task) => (
|
||||
<div className={"flex items-center gap-2"} key={task.id}>
|
||||
<input
|
||||
onChange={() => handleChangeStatus(todolist.id, task)}
|
||||
type={"checkbox"}
|
||||
checked={[0, 1, 3].includes(task.status)}
|
||||
/>
|
||||
<div key={task.id}>{task.title}</div>
|
||||
<button onClick={() => handleDeleteTask(todolist.id, task.id)}>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<Button type={"submit"} variant={"icon"} className={"absolute right-4"}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</form>
|
||||
{filteredTasks?.map((task) => {
|
||||
return (
|
||||
<div className={"flex cursor-pointer items-center"} key={task.id}>
|
||||
<label className={"flex cursor-pointer items-center gap-2"}>
|
||||
<input
|
||||
onChange={() => handleChangeStatus(todolist.id, task)}
|
||||
type={"checkbox"}
|
||||
checked={TaskStatus.DONE === task.status}
|
||||
/>
|
||||
{task.title}
|
||||
</label>
|
||||
<Button
|
||||
variant={"icon"}
|
||||
onClick={() => handleDeleteTask(todolist.id, task.id)}
|
||||
className={"ml-2"}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={"flex"}>
|
||||
<Button onClick={handleFilterChange} value={"all"}>
|
||||
All
|
||||
</Button>
|
||||
<Button onClick={handleFilterChange} value={"active"}>
|
||||
Active
|
||||
</Button>
|
||||
<Button onClick={handleFilterChange} value={"completed"}>
|
||||
Completed
|
||||
</Button>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
onValueChange={handleFilterChange}
|
||||
value={filter}
|
||||
className={"w-full"}
|
||||
>
|
||||
<ToggleGroupItem className={"w-full"} value="all">
|
||||
All
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem className={"w-full"} value="active">
|
||||
Active
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem className={"w-full"} value="completed">
|
||||
Completed
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
83
src/components/toggle-group/toggle-group.tsx
Normal file
83
src/components/toggle-group/toggle-group.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
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 { 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",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-3",
|
||||
sm: "h-8 px-2",
|
||||
lg: "h-10 px-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
const ToggleGroup = React.forwardRef<
|
||||
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, children, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("flex items-center justify-center gap-1", className)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
));
|
||||
|
||||
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);
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
);
|
||||
});
|
||||
|
||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
6
src/env/client.mjs
vendored
6
src/env/client.mjs
vendored
@@ -5,7 +5,7 @@ const _clientEnv = clientSchema.safeParse(clientEnv);
|
||||
|
||||
export const formatErrors = (
|
||||
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
|
||||
errors,
|
||||
errors
|
||||
) =>
|
||||
Object.entries(errors)
|
||||
.map(([name, value]) => {
|
||||
@@ -17,7 +17,7 @@ export const formatErrors = (
|
||||
if (!_clientEnv.success) {
|
||||
console.error(
|
||||
"❌ Invalid environment variables:\n",
|
||||
...formatErrors(_clientEnv.error.format()),
|
||||
...formatErrors(_clientEnv.error.format())
|
||||
);
|
||||
throw new Error("Invalid environment variables");
|
||||
}
|
||||
@@ -25,7 +25,7 @@ if (!_clientEnv.success) {
|
||||
for (let 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");
|
||||
|
||||
2
src/env/server.mjs
vendored
2
src/env/server.mjs
vendored
@@ -11,7 +11,7 @@ const _serverEnv = serverSchema.safeParse(process.env);
|
||||
if (!_serverEnv.success) {
|
||||
console.error(
|
||||
"❌ Invalid environment variables:\n",
|
||||
...formatErrors(_serverEnv.error.format()),
|
||||
...formatErrors(_serverEnv.error.format())
|
||||
);
|
||||
throw new Error("Invalid environment variables");
|
||||
}
|
||||
|
||||
6
src/helpers/classnames/classnames.ts
Normal file
6
src/helpers/classnames/classnames.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
1
src/helpers/classnames/index.ts
Normal file
1
src/helpers/classnames/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./classnames";
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { ApiResponse } from "@/helpers";
|
||||
|
||||
export const handleError = <T>(data: ApiResponse<T>): ApiResponse<T> => {
|
||||
if (data.resultCode !== 0) {
|
||||
const error = data?.messages?.[0] || "An error has occurred";
|
||||
|
||||
throw new Error(error);
|
||||
}
|
||||
// if (data.resultCode !== 0) {
|
||||
// const error = data?.messages?.[0] || "An error has occurred";
|
||||
//
|
||||
// throw new Error(error);
|
||||
// }
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./handle-error";
|
||||
export * from "./no-refetch";
|
||||
export * from "./api-response";
|
||||
export * from "./classnames";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ChangeEvent } from "react";
|
||||
import type { ChangeEvent, FormEvent } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { type NextPage } from "next";
|
||||
@@ -23,7 +23,8 @@ const Home: NextPage = () => {
|
||||
|
||||
const { mutate: createTodolist } = useCreateTodolistMutation();
|
||||
|
||||
const handleAddTodolist = () => {
|
||||
const handleAddTodolist = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
createTodolist({ title: newTodolistTitle });
|
||||
setNewTodolistTitle("");
|
||||
};
|
||||
@@ -46,7 +47,7 @@ const Home: NextPage = () => {
|
||||
<Button onClick={handleLogout}>Logout</Button>
|
||||
</header>
|
||||
<main className={"flex flex-col gap-4 p-4"}>
|
||||
<div className={"flex items-end gap-2.5"}>
|
||||
<form onSubmit={handleAddTodolist} className={"flex items-end gap-2.5"}>
|
||||
<label className={"flex w-52 flex-col gap-0.5"}>
|
||||
new todolist
|
||||
<Input
|
||||
@@ -54,8 +55,8 @@ const Home: NextPage = () => {
|
||||
onChange={handleNewTodolistTitleChange}
|
||||
/>
|
||||
</label>
|
||||
<Button onClick={handleAddTodolist}>+</Button>
|
||||
</div>
|
||||
<Button type={"submit"}>+</Button>
|
||||
</form>
|
||||
<div className={"flex flex-wrap gap-4"}>
|
||||
{todolists?.map((todolist) => {
|
||||
return <Todolist todolist={todolist} key={todolist.id} />;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { handleError } from "@/helpers";
|
||||
import { authInstance } from "@/services/auth/auth.instance";
|
||||
import type {
|
||||
LoginResponse,
|
||||
LogoutResponse,
|
||||
MeResponse,
|
||||
PostLoginArgs,
|
||||
} from "@/services/todolists";
|
||||
|
||||
export const AuthApi = {
|
||||
async login(args: PostLoginArgs) {
|
||||
const res = await authInstance.post<LoginResponse>("/login", args);
|
||||
|
||||
return handleError(res.data);
|
||||
},
|
||||
|
||||
async logout() {
|
||||
const res = await authInstance.delete<LogoutResponse>("/login");
|
||||
|
||||
return handleError(res.data);
|
||||
},
|
||||
|
||||
async me() {
|
||||
const res = await authInstance.get<MeResponse>("/me");
|
||||
|
||||
return handleError(res.data);
|
||||
},
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const authInstance = axios.create({
|
||||
baseURL: "https://social-network.samuraijs.com/api/1.1/auth/",
|
||||
withCredentials: true,
|
||||
});
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./auth";
|
||||
export * from "./todolists";
|
||||
export * from "./todolist-api/auth";
|
||||
export * from "./todolist-api/todolists";
|
||||
|
||||
35
src/services/todolist-api/auth/auth.api.ts
Normal file
35
src/services/todolist-api/auth/auth.api.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type {
|
||||
LoginResponse,
|
||||
LogoutResponse,
|
||||
MeResponse,
|
||||
PostLoginArgs,
|
||||
} from "../todolists";
|
||||
|
||||
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);
|
||||
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async logout() {
|
||||
const res = await todolistApiInstance.delete<LogoutResponse>("/auth/login");
|
||||
|
||||
return handleError(res.data);
|
||||
},
|
||||
|
||||
async me() {
|
||||
const res = await todolistApiInstance.get<MeResponse>("/auth/me");
|
||||
|
||||
return handleError(res.data);
|
||||
},
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
|
||||
|
||||
import { QUERY_KEYS, ROUTES } from "@/constants";
|
||||
import { noRefetch } from "@/helpers";
|
||||
import { AuthApi } from "@/services/auth/auth.api";
|
||||
import { AuthApi } from "@/services/todolist-api/auth/auth.api";
|
||||
|
||||
export const useMeQuery = () => {
|
||||
return useQuery({
|
||||
90
src/services/todolist-api/todolist-api.instance.ts
Normal file
90
src/services/todolist-api/todolist-api.instance.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Mutex } from "async-mutex";
|
||||
import axios from "axios";
|
||||
const mutex = new Mutex();
|
||||
|
||||
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;
|
||||
}>(
|
||||
"/auth/refresh-token",
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("refreshToken")}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
localStorage.setItem("accessToken", res.data.accessToken);
|
||||
localStorage.setItem("refreshToken", res.data.refreshToken);
|
||||
|
||||
return res.data.accessToken;
|
||||
}
|
||||
|
||||
todolistApiInstance.interceptors.request.use(
|
||||
async (config) => {
|
||||
if (!config?.url?.includes("refresh-token")) {
|
||||
await mutex.waitForUnlock();
|
||||
}
|
||||
|
||||
config.headers.Authorization =
|
||||
config.headers.Authorization ??
|
||||
`Bearer ${localStorage.getItem("accessToken")}`;
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
todolistApiInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
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;
|
||||
// Use a mutex to ensure that token refresh logic is not run multiple times in parallel
|
||||
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();
|
||||
|
||||
return todolistApiInstance(originalRequest);
|
||||
}
|
||||
refreshedAt = new Date().getTime();
|
||||
try {
|
||||
const newAccessToken = await refreshAccessToken();
|
||||
|
||||
originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`;
|
||||
|
||||
return todolistApiInstance(originalRequest); // Retry the original request with the new token
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
} else {
|
||||
await mutex.waitForUnlock();
|
||||
const newAccessToken = localStorage.getItem("accessToken");
|
||||
|
||||
originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`;
|
||||
|
||||
return todolistApiInstance(originalRequest); // Retry the original request with the new token
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
@@ -1,5 +1,3 @@
|
||||
import { todolistsInstance } from "./todolists.instance";
|
||||
|
||||
import { handleError } from "@/helpers";
|
||||
import type {
|
||||
CreateTaskResponse,
|
||||
@@ -11,33 +9,37 @@ import type {
|
||||
Todolist,
|
||||
UpdateTaskResponse,
|
||||
} from "@/services";
|
||||
import { todolistApiInstance } from "@/services/todolist-api/todolist-api.instance";
|
||||
|
||||
export const TodolistAPI = {
|
||||
async getTodolists() {
|
||||
const res = await todolistsInstance.get<Todolist[]>("/");
|
||||
const res = await todolistApiInstance.get<Todolist[]>("/todolists");
|
||||
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async createTodolist({ title }: { title: string }) {
|
||||
const res = await todolistsInstance.post<CreateTodolistResponse>("/", {
|
||||
title,
|
||||
});
|
||||
const res = await todolistApiInstance.post<CreateTodolistResponse>(
|
||||
"/todolists",
|
||||
{
|
||||
title,
|
||||
}
|
||||
);
|
||||
|
||||
return handleError(res.data);
|
||||
},
|
||||
|
||||
async deleteTodolist({ todolistId }: { todolistId: string }) {
|
||||
const res = await todolistsInstance.delete<DeleteTodolistResponse>(
|
||||
`/${todolistId}`
|
||||
const res = await todolistApiInstance.delete<DeleteTodolistResponse>(
|
||||
`/todolists/${todolistId}`
|
||||
);
|
||||
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async getTodolistTasks({ todolistId }: { todolistId: string }) {
|
||||
const res = await todolistsInstance.get<TasksResponse>(
|
||||
`/${todolistId}/tasks`
|
||||
const res = await todolistApiInstance.get<TasksResponse>(
|
||||
`/todolists/${todolistId}/tasks`
|
||||
);
|
||||
|
||||
return res.data;
|
||||
@@ -50,18 +52,18 @@ export const TodolistAPI = {
|
||||
todolistId: string;
|
||||
title: string;
|
||||
}) {
|
||||
const res = await todolistsInstance.post<CreateTaskResponse>(
|
||||
`/${todolistId}/tasks`,
|
||||
const res = await todolistApiInstance.post<CreateTaskResponse>(
|
||||
`/todolists/${todolistId}/tasks`,
|
||||
{ title }
|
||||
);
|
||||
|
||||
return handleError(res.data);
|
||||
return res.data;
|
||||
},
|
||||
|
||||
async updateTask({ todolistId, task }: { todolistId: string; task: Task }) {
|
||||
const { id, ...rest } = task;
|
||||
const res = await todolistsInstance.put<UpdateTaskResponse>(
|
||||
`/${todolistId}/tasks/${id}`,
|
||||
const res = await todolistApiInstance.patch<UpdateTaskResponse>(
|
||||
`/todolists/${todolistId}/tasks/${id}`,
|
||||
rest
|
||||
);
|
||||
|
||||
@@ -75,8 +77,8 @@ export const TodolistAPI = {
|
||||
todolistId: string;
|
||||
taskId: string;
|
||||
}) {
|
||||
const res = await todolistsInstance.delete<DeleteTaskResponse>(
|
||||
`/${todolistId}/tasks/${taskId}`
|
||||
const res = await todolistApiInstance.delete<DeleteTaskResponse>(
|
||||
`/todolists/${todolistId}/tasks/${taskId}`
|
||||
);
|
||||
|
||||
return res.data;
|
||||
@@ -48,7 +48,7 @@ export const useCreateTaskMutation = () => {
|
||||
return useMutation({
|
||||
mutationFn: TodolistAPI.createTask,
|
||||
onSuccess: (res) => {
|
||||
const todolistId = res.data.item.todoListId;
|
||||
const todolistId = res.todoListId;
|
||||
|
||||
queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId]);
|
||||
},
|
||||
@@ -60,9 +60,7 @@ export const useUpdateTaskMutation = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: TodolistAPI.updateTask,
|
||||
onSuccess: async (res) => {
|
||||
const todolistId = res.data.item.todoListId;
|
||||
|
||||
onSuccess: async (_, { todolistId }) => {
|
||||
await queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId]);
|
||||
},
|
||||
});
|
||||
@@ -1,26 +1,19 @@
|
||||
import type { ApiResponse } from "@/helpers";
|
||||
|
||||
export type UpdateTaskResponseData = {
|
||||
item: Task;
|
||||
};
|
||||
|
||||
export type TasksResponse = {
|
||||
items: Task[];
|
||||
totalCount: number;
|
||||
error?: string;
|
||||
};
|
||||
export type TasksResponse = Task[];
|
||||
|
||||
export type Task = {
|
||||
id: string;
|
||||
order: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
description?: string | null;
|
||||
deadline?: string | null;
|
||||
status: TaskStatus;
|
||||
priority: TaskPriority;
|
||||
ownerId: string;
|
||||
todoListId: string;
|
||||
order: number;
|
||||
status: number;
|
||||
priority: number;
|
||||
startDate?: Date;
|
||||
deadline?: Date;
|
||||
addedDate: Date;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type Todolist = {
|
||||
@@ -37,7 +30,8 @@ export type PostLoginArgs = {
|
||||
};
|
||||
|
||||
export type LoginResponseData = {
|
||||
userId: number;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
export type MeResponseData = {
|
||||
@@ -46,7 +40,7 @@ export type MeResponseData = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type LoginResponse = ApiResponse<LoginResponseData>;
|
||||
export type LoginResponse = LoginResponseData;
|
||||
export type LogoutResponse = ApiResponse<never>;
|
||||
export type MeResponse = ApiResponse<MeResponseData>;
|
||||
|
||||
@@ -56,6 +50,18 @@ export type CreateTodolistResponseData = {
|
||||
item: Todolist;
|
||||
};
|
||||
|
||||
export type CreateTaskResponse = ApiResponse<{ item: Task }>;
|
||||
export type DeleteTaskResponse = ApiResponse<never>;
|
||||
export type UpdateTaskResponse = ApiResponse<UpdateTaskResponseData>;
|
||||
export type CreateTaskResponse = Task;
|
||||
export type DeleteTaskResponse = void;
|
||||
export type UpdateTaskResponse = void;
|
||||
|
||||
export enum TaskStatus {
|
||||
OPEN = "OPEN",
|
||||
IN_PROGRESS = "IN_PROGRESS",
|
||||
DONE = "DONE",
|
||||
}
|
||||
|
||||
export enum TaskPriority {
|
||||
LOW = "LOW",
|
||||
MEDIUM = "MEDIUM",
|
||||
HIGH = "HIGH",
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import axios from "axios";
|
||||
|
||||
export const todolistsInstance = axios.create({
|
||||
baseURL: "https://social-network.samuraijs.com/api/1.1/todo-lists",
|
||||
withCredentials: true,
|
||||
});
|
||||
@@ -2,47 +2,47 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px dotted #000;
|
||||
border-style: solid solid dotted dotted;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 2s linear infinite;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px dotted #000;
|
||||
border-style: solid solid dotted dotted;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 2s linear infinite;
|
||||
}
|
||||
.loader::after {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
border: 3px dotted #FF3D00;
|
||||
border-style: solid solid dotted;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
animation: rotationBack 1s linear infinite;
|
||||
transform-origin: center center;
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
border: 3px dotted #ff3d00;
|
||||
border-style: solid solid dotted;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
animation: rotationBack 1s linear infinite;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes rotationBack {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"],
|
||||
"exclude": ["node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user