mirror of
https://github.com/ershisan99/todolist_next.git
synced 2025-12-16 20:59:24 +00:00
lint and format
This commit is contained in:
6
.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/jsLibraryMappings.xml
generated
Normal 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>
|
||||||
@@ -9,5 +9,10 @@
|
|||||||
"recommended": true
|
"recommended": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"javascript": {
|
||||||
|
"formatter": {
|
||||||
|
"semicolons": "asNeeded"
|
||||||
|
}
|
||||||
|
},
|
||||||
"formatter": { "indentStyle": "space", "indentWidth": 2 }
|
"formatter": { "indentStyle": "space", "indentWidth": 2 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
import type { FC, ReactNode } from "react";
|
import type { FC, ReactNode } from "react"
|
||||||
import { useEffect } 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 }) => {
|
export const AuthRedirect: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const { data: user, isLoading } = useMeQuery();
|
const { data: user, isLoading } = useMeQuery()
|
||||||
const isAuthPage =
|
const isAuthPage =
|
||||||
router.pathname === "/login" || router.pathname === "/sign-up";
|
router.pathname === "/login" || router.pathname === "/sign-up"
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading && !user && !isAuthPage) {
|
if (!isLoading && !user && !isAuthPage) {
|
||||||
router.push("/login");
|
router.push("/login")
|
||||||
}
|
}
|
||||||
}, [user, isLoading, isAuthPage, router]);
|
}, [user, isLoading, isAuthPage, router])
|
||||||
|
|
||||||
if (isLoading || (!user && !isAuthPage)) {
|
if (isLoading || (!user && !isAuthPage)) {
|
||||||
return (
|
return (
|
||||||
<div className={"h-screen"}>
|
<div className={"h-screen"}>
|
||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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"> & {
|
type Props = ComponentPropsWithoutRef<"button"> & {
|
||||||
variant?: "primary" | "outlined" | "icon";
|
variant?: "primary" | "outlined" | "icon"
|
||||||
};
|
}
|
||||||
|
|
||||||
export const Button: FC<Props> = ({
|
export const Button: FC<Props> = ({
|
||||||
className,
|
className,
|
||||||
@@ -18,9 +18,9 @@ export const Button: FC<Props> = ({
|
|||||||
variant === "outlined" && "border-sky-700 bg-inherit text-sky-700",
|
variant === "outlined" && "border-sky-700 bg-inherit text-sky-700",
|
||||||
variant === "icon" &&
|
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",
|
"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}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Loader } from "../loader";
|
import { Loader } from "../loader"
|
||||||
|
|
||||||
export const FullscreenLoader = () => {
|
export const FullscreenLoader = () => {
|
||||||
return (
|
return (
|
||||||
<div className={"flex h-screen items-center justify-center"}>
|
<div className={"flex h-screen items-center justify-center"}>
|
||||||
<Loader />
|
<Loader />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * from "./todolist";
|
export * from "./todolist"
|
||||||
export * from "./auth-redirect";
|
export * from "./auth-redirect"
|
||||||
export * from "./button";
|
export * from "./button"
|
||||||
export * from "./input";
|
export * from "./input"
|
||||||
export * from "./loader";
|
export * from "./loader"
|
||||||
export * from "./fullscreen-loader";
|
export * from "./fullscreen-loader"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { DetailedHTMLProps, FC, InputHTMLAttributes } from "react";
|
import type { DetailedHTMLProps, FC, InputHTMLAttributes } from "react"
|
||||||
|
|
||||||
type Props = DetailedHTMLProps<
|
type Props = DetailedHTMLProps<
|
||||||
InputHTMLAttributes<HTMLInputElement>,
|
InputHTMLAttributes<HTMLInputElement>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
>;
|
>
|
||||||
|
|
||||||
export const Input: FC<Props> = ({ className, ...rest }) => {
|
export const Input: FC<Props> = ({ className, ...rest }) => {
|
||||||
return (
|
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}`}
|
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}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ export const Loader = () => {
|
|||||||
<div className={"flex h-full w-full items-center justify-center"}>
|
<div className={"flex h-full w-full items-center justify-center"}>
|
||||||
<span className="loader" />
|
<span className="loader" />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import type { ChangeEvent, FC, FormEvent, MouseEventHandler } from "react";
|
import type { ChangeEvent, FC, FormEvent } from "react"
|
||||||
import { memo, useState } from "react";
|
import { memo, useState } from "react"
|
||||||
|
|
||||||
import { Trash, Plus } from "lucide-react";
|
import { Plus, Trash } from "lucide-react"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Task,
|
Task,
|
||||||
Todolist as TodolistType,
|
Todolist as TodolistType,
|
||||||
} from "../../services/todolist-api/todolists";
|
} from "../../services/todolist-api/todolists"
|
||||||
import { Button } from "../button";
|
import { Button } from "../button"
|
||||||
import { Input } from "../input";
|
import { Input } from "../input"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ToggleGroup,
|
ToggleGroup,
|
||||||
ToggleGroupItem,
|
ToggleGroupItem,
|
||||||
} from "@/components/toggle-group/toggle-group";
|
} from "@/components/toggle-group/toggle-group"
|
||||||
import {
|
import {
|
||||||
TaskStatus,
|
TaskStatus,
|
||||||
useCreateTaskMutation,
|
useCreateTaskMutation,
|
||||||
@@ -21,62 +21,62 @@ import {
|
|||||||
useDeleteTodolistMutation,
|
useDeleteTodolistMutation,
|
||||||
useGetTasksQuery,
|
useGetTasksQuery,
|
||||||
useUpdateTaskMutation,
|
useUpdateTaskMutation,
|
||||||
} from "@/services";
|
} from "@/services"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
todolist: TodolistType;
|
todolist: TodolistType
|
||||||
};
|
}
|
||||||
|
|
||||||
type Filter = "all" | "active" | "completed";
|
type Filter = "all" | "active" | "completed"
|
||||||
|
|
||||||
export const Todolist: FC<Props> = memo(({ todolist }) => {
|
export const Todolist: FC<Props> = memo(({ todolist }) => {
|
||||||
const { data: tasks, isLoading } = useGetTasksQuery(todolist.id);
|
const { data: tasks, isLoading } = useGetTasksQuery(todolist.id)
|
||||||
const { mutate: putTask } = useUpdateTaskMutation();
|
const { mutate: putTask } = useUpdateTaskMutation()
|
||||||
const { mutate: deleteTask } = useDeleteTaskMutation();
|
const { mutate: deleteTask } = useDeleteTaskMutation()
|
||||||
const { mutate: deleteTodolist } = useDeleteTodolistMutation();
|
const { mutate: deleteTodolist } = useDeleteTodolistMutation()
|
||||||
const { mutate: createTask } = useCreateTaskMutation();
|
const { mutate: createTask } = useCreateTaskMutation()
|
||||||
const [newTaskTitle, setNewTaskTitle] = useState("");
|
const [newTaskTitle, setNewTaskTitle] = useState("")
|
||||||
const [filter, setFilter] = useState("all");
|
const [filter, setFilter] = useState("all")
|
||||||
|
|
||||||
const handleChangeStatus = (todolistId: string, task: Task) => {
|
const handleChangeStatus = (todolistId: string, task: Task) => {
|
||||||
const newTask: Task = {
|
const newTask: Task = {
|
||||||
...task,
|
...task,
|
||||||
status:
|
status:
|
||||||
task.status === TaskStatus.DONE ? TaskStatus.OPEN : TaskStatus.DONE,
|
task.status === TaskStatus.DONE ? TaskStatus.OPEN : TaskStatus.DONE,
|
||||||
};
|
}
|
||||||
|
|
||||||
putTask({ todolistId, task: newTask });
|
putTask({ todolistId, task: newTask })
|
||||||
};
|
}
|
||||||
const handleDeleteTask = (todolistId: string, taskId: string) => {
|
const handleDeleteTask = (todolistId: string, taskId: string) => {
|
||||||
deleteTask({ todolistId, taskId });
|
deleteTask({ todolistId, taskId })
|
||||||
};
|
}
|
||||||
const handleDeleteTodolist = (todolistId: string) => {
|
const handleDeleteTodolist = (todolistId: string) => {
|
||||||
deleteTodolist({ todolistId });
|
deleteTodolist({ todolistId })
|
||||||
};
|
}
|
||||||
const handleAddTask = (e: FormEvent<HTMLFormElement>) => {
|
const handleAddTask = (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
createTask({ todolistId: todolist.id, title: newTaskTitle });
|
createTask({ todolistId: todolist.id, title: newTaskTitle })
|
||||||
setNewTaskTitle("");
|
setNewTaskTitle("")
|
||||||
};
|
}
|
||||||
const handleNewTaskTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleNewTaskTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setNewTaskTitle(e.target.value);
|
setNewTaskTitle(e.target.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleFilterChange = (value: string) => {
|
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) => {
|
const filteredTasks = tasks?.filter((task) => {
|
||||||
switch (filter) {
|
switch (filter) {
|
||||||
case "active":
|
case "active":
|
||||||
return task.status === TaskStatus.OPEN;
|
return task.status === TaskStatus.OPEN
|
||||||
case "completed":
|
case "completed":
|
||||||
return task.status === TaskStatus.DONE;
|
return task.status === TaskStatus.DONE
|
||||||
default:
|
default:
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -122,7 +122,7 @@ export const Todolist: FC<Props> = memo(({ todolist }) => {
|
|||||||
<Trash size={16} />
|
<Trash size={16} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
})}
|
})}
|
||||||
<div className={"flex"}>
|
<div className={"flex"}>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
@@ -143,5 +143,5 @@ export const Todolist: FC<Props> = memo(({ todolist }) => {
|
|||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -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 * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||||
import type { VariantProps } from "class-variance-authority";
|
import type { VariantProps } from "class-variance-authority"
|
||||||
import { cva } from "class-variance-authority";
|
import { cva } from "class-variance-authority"
|
||||||
|
|
||||||
import { cn } from "@/helpers";
|
import { cn } from "@/helpers"
|
||||||
|
|
||||||
const toggleVariants = cva(
|
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",
|
"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",
|
variant: "default",
|
||||||
size: "default",
|
size: "default",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
const ToggleGroupContext = React.createContext<
|
const ToggleGroupContext = React.createContext<
|
||||||
VariantProps<typeof toggleVariants>
|
VariantProps<typeof toggleVariants>
|
||||||
>({
|
>({
|
||||||
size: "default",
|
size: "default",
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
})
|
||||||
|
|
||||||
const ToggleGroup = React.forwardRef<
|
const ToggleGroup = React.forwardRef<
|
||||||
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
|
||||||
@@ -50,16 +50,16 @@ const ToggleGroup = React.forwardRef<
|
|||||||
{children}
|
{children}
|
||||||
</ToggleGroupContext.Provider>
|
</ToggleGroupContext.Provider>
|
||||||
</ToggleGroupPrimitive.Root>
|
</ToggleGroupPrimitive.Root>
|
||||||
));
|
))
|
||||||
|
|
||||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
|
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
|
||||||
|
|
||||||
const ToggleGroupItem = React.forwardRef<
|
const ToggleGroupItem = React.forwardRef<
|
||||||
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
|
||||||
VariantProps<typeof toggleVariants>
|
VariantProps<typeof toggleVariants>
|
||||||
>(({ className, children, variant, size, ...props }, ref) => {
|
>(({ className, children, variant, size, ...props }, ref) => {
|
||||||
const context = React.useContext(ToggleGroupContext);
|
const context = React.useContext(ToggleGroupContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToggleGroupPrimitive.Item
|
<ToggleGroupPrimitive.Item
|
||||||
@@ -69,15 +69,15 @@ const ToggleGroupItem = React.forwardRef<
|
|||||||
variant: context.variant || variant,
|
variant: context.variant || variant,
|
||||||
size: context.size || size,
|
size: context.size || size,
|
||||||
}),
|
}),
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ToggleGroupPrimitive.Item>
|
</ToggleGroupPrimitive.Item>
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
|
||||||
|
|
||||||
export { ToggleGroup, ToggleGroupItem };
|
export { ToggleGroup, ToggleGroupItem }
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from "./routes";
|
export * from "./routes"
|
||||||
export * from "./query-keys";
|
export * from "./query-keys"
|
||||||
export * from "./result-code";
|
export * from "./result-code"
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ export const QUERY_KEYS = {
|
|||||||
TODOLISTS: "todolists",
|
TODOLISTS: "todolists",
|
||||||
TASKS: "tasks",
|
TASKS: "tasks",
|
||||||
ME: "me",
|
ME: "me",
|
||||||
} as const;
|
} as const
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const ROUTES = {
|
export const ROUTES = {
|
||||||
HOME: "/",
|
HOME: "/",
|
||||||
LOGIN: "/login",
|
LOGIN: "/login",
|
||||||
} as const;
|
} as const
|
||||||
|
|||||||
26
src/env/client.mjs
vendored
26
src/env/client.mjs
vendored
@@ -1,35 +1,35 @@
|
|||||||
// @ts-check
|
// @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 = (
|
export const formatErrors = (
|
||||||
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
|
/** @type {import('zod').ZodFormattedError<Map<string,string>,string>} */
|
||||||
errors
|
errors,
|
||||||
) =>
|
) =>
|
||||||
Object.entries(errors)
|
Object.entries(errors)
|
||||||
.map(([name, value]) => {
|
.map(([name, value]) => {
|
||||||
if (value && "_errors" in 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) {
|
if (!_clientEnv.success) {
|
||||||
console.error(
|
console.error(
|
||||||
"❌ Invalid environment variables:\n",
|
"❌ Invalid environment variables:\n",
|
||||||
...formatErrors(_clientEnv.error.format())
|
...formatErrors(_clientEnv.error.format()),
|
||||||
);
|
)
|
||||||
throw new Error("Invalid environment variables");
|
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_")) {
|
if (!key.startsWith("NEXT_PUBLIC_")) {
|
||||||
console.warn(
|
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
8
src/env/schema.mjs
vendored
@@ -1,5 +1,5 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import { z } from "zod";
|
import { z } from "zod"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specify your server-side environment variables schema here.
|
* Specify your server-side environment variables schema here.
|
||||||
@@ -7,7 +7,7 @@ import { z } from "zod";
|
|||||||
*/
|
*/
|
||||||
export const serverSchema = z.object({
|
export const serverSchema = z.object({
|
||||||
NODE_ENV: z.enum(["development", "test", "production"]),
|
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||||
});
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specify your client-side environment variables schema here.
|
* Specify your client-side environment variables schema here.
|
||||||
@@ -16,7 +16,7 @@ export const serverSchema = z.object({
|
|||||||
*/
|
*/
|
||||||
export const clientSchema = z.object({
|
export const clientSchema = z.object({
|
||||||
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
// NEXT_PUBLIC_CLIENTVAR: z.string(),
|
||||||
});
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* You can't destruct `process.env` as a regular object, so you have to do
|
* 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 = {
|
export const clientEnv = {
|
||||||
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||||
};
|
}
|
||||||
|
|||||||
20
src/env/server.mjs
vendored
20
src/env/server.mjs
vendored
@@ -3,25 +3,25 @@
|
|||||||
* This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars.
|
* 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.
|
* It has to be a `.mjs`-file to be imported there.
|
||||||
*/
|
*/
|
||||||
import { serverSchema } from "./schema.mjs";
|
import { serverSchema } from "./schema.mjs"
|
||||||
import { env as clientEnv, formatErrors } from "./client.mjs";
|
import { env as clientEnv, formatErrors } from "./client.mjs"
|
||||||
|
|
||||||
const _serverEnv = serverSchema.safeParse(process.env);
|
const _serverEnv = serverSchema.safeParse(process.env)
|
||||||
|
|
||||||
if (!_serverEnv.success) {
|
if (!_serverEnv.success) {
|
||||||
console.error(
|
console.error(
|
||||||
"❌ Invalid environment variables:\n",
|
"❌ Invalid environment variables:\n",
|
||||||
...formatErrors(_serverEnv.error.format())
|
...formatErrors(_serverEnv.error.format()),
|
||||||
);
|
)
|
||||||
throw new Error("Invalid environment variables");
|
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_")) {
|
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 }
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import type { ResultCode } from "@/constants";
|
import type { ResultCode } from "@/constants"
|
||||||
|
|
||||||
type ApiResponseSuccess<T> = {
|
type ApiResponseSuccess<T> = {
|
||||||
data: T;
|
data: T
|
||||||
messages?: never;
|
messages?: never
|
||||||
fieldsErrors?: never;
|
fieldsErrors?: never
|
||||||
resultCode: ResultCode.Success;
|
resultCode: ResultCode.Success
|
||||||
};
|
}
|
||||||
|
|
||||||
type ApiResponseError<T> = {
|
type ApiResponseError<T> = {
|
||||||
data: T;
|
data: T
|
||||||
messages: string[];
|
messages: string[]
|
||||||
fieldsErrors?: string[];
|
fieldsErrors?: string[]
|
||||||
resultCode: ResultCode.InvalidRequest | ResultCode.InvalidRequestWithCaptcha;
|
resultCode: ResultCode.InvalidRequest | ResultCode.InvalidRequestWithCaptcha
|
||||||
};
|
}
|
||||||
|
|
||||||
export type ApiResponse<T> = ApiResponseSuccess<T> | ApiResponseError<T>;
|
export type ApiResponse<T> = ApiResponseSuccess<T> | ApiResponseError<T>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from "./classnames";
|
export * from "./classnames"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ApiResponse } from "@/helpers";
|
import type { ApiResponse } from "@/helpers"
|
||||||
|
|
||||||
export const handleError = <T>(data: ApiResponse<T>): ApiResponse<T> => {
|
export const handleError = <T>(data: ApiResponse<T>): ApiResponse<T> => {
|
||||||
// if (data.resultCode !== 0) {
|
// if (data.resultCode !== 0) {
|
||||||
@@ -7,5 +7,5 @@ export const handleError = <T>(data: ApiResponse<T>): ApiResponse<T> => {
|
|||||||
// throw new Error(error);
|
// throw new Error(error);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return data;
|
return data
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from "./handle-error";
|
export * from "./handle-error"
|
||||||
export * from "./no-refetch";
|
export * from "./no-refetch"
|
||||||
export * from "./api-response";
|
export * from "./api-response"
|
||||||
export * from "./classnames";
|
export * from "./classnames"
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ export const noRefetch = {
|
|||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
refetchIntervalInBackground: false,
|
refetchIntervalInBackground: false,
|
||||||
retry: false,
|
retry: false,
|
||||||
} as const;
|
} as const
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import "../styles/globals.css";
|
import "../styles/globals.css"
|
||||||
|
|
||||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
import { QueryClientProvider, QueryClient } from "@tanstack/react-query"
|
||||||
import { type AppType } from "next/dist/shared/lib/utils";
|
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 }) => {
|
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||||
return (
|
return (
|
||||||
@@ -14,7 +14,7 @@ const MyApp: AppType = ({ Component, pageProps }) => {
|
|||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</AuthRedirect>
|
</AuthRedirect>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default MyApp;
|
export default MyApp
|
||||||
|
|||||||
@@ -1,39 +1,38 @@
|
|||||||
import type { ChangeEvent, FormEvent } from "react";
|
import type { ChangeEvent, FormEvent } from "react"
|
||||||
import { useState } from "react";
|
import { useState } from "react"
|
||||||
|
|
||||||
import { type NextPage } from "next";
|
import type { NextPage } from "next"
|
||||||
import Head from "next/head";
|
import Head from "next/head"
|
||||||
|
|
||||||
import { Todolist, Button, FullscreenLoader, Input } from "@/components";
|
import { Todolist, Button, FullscreenLoader, Input } from "@/components"
|
||||||
import {
|
import {
|
||||||
useCreateTodolistMutation,
|
useCreateTodolistMutation,
|
||||||
useLogoutMutation,
|
useLogoutMutation,
|
||||||
useTodolistsQuery,
|
useTodolistsQuery,
|
||||||
} from "@/services";
|
} from "@/services"
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const Home: NextPage = () => {
|
||||||
const [newTodolistTitle, setNewTodolistTitle] = useState("");
|
const [newTodolistTitle, setNewTodolistTitle] = useState("")
|
||||||
const { mutate: logout } = useLogoutMutation();
|
const { mutate: logout } = useLogoutMutation()
|
||||||
const { data: todolists, isLoading: isTodolistsLoading } =
|
const { data: todolists, isLoading: isTodolistsLoading } = useTodolistsQuery()
|
||||||
useTodolistsQuery();
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout();
|
logout()
|
||||||
};
|
}
|
||||||
|
|
||||||
const { mutate: createTodolist } = useCreateTodolistMutation();
|
const { mutate: createTodolist } = useCreateTodolistMutation()
|
||||||
|
|
||||||
const handleAddTodolist = (e: FormEvent<HTMLFormElement>) => {
|
const handleAddTodolist = (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
createTodolist({ title: newTodolistTitle });
|
createTodolist({ title: newTodolistTitle })
|
||||||
setNewTodolistTitle("");
|
setNewTodolistTitle("")
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleNewTodolistTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleNewTodolistTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setNewTodolistTitle(e.target.value);
|
setNewTodolistTitle(e.target.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
if (isTodolistsLoading) return <FullscreenLoader />;
|
if (isTodolistsLoading) return <FullscreenLoader />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -59,12 +58,12 @@ const Home: NextPage = () => {
|
|||||||
</form>
|
</form>
|
||||||
<div className={"flex flex-wrap gap-4"}>
|
<div className={"flex flex-wrap gap-4"}>
|
||||||
{todolists?.map((todolist) => {
|
{todolists?.map((todolist) => {
|
||||||
return <Todolist todolist={todolist} key={todolist.id} />;
|
return <Todolist todolist={todolist} key={todolist.id} />
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Home;
|
export default Home
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
import type { ChangeEvent } from "react";
|
import type { ChangeEvent } from "react"
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react"
|
||||||
|
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next"
|
||||||
|
|
||||||
import { Button, Input } from "@/components";
|
import { Button, Input } from "@/components"
|
||||||
import { useLoginMutation } from "@/services";
|
import { useLoginMutation } from "@/services"
|
||||||
|
|
||||||
const Login: NextPage = () => {
|
const Login: NextPage = () => {
|
||||||
const { mutate: login } = useLoginMutation();
|
const { mutate: login } = useLoginMutation()
|
||||||
|
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("")
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("")
|
||||||
const [remember, setRemember] = useState(true);
|
const [remember, setRemember] = useState(true)
|
||||||
|
|
||||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setPassword(e.target.value);
|
setPassword(e.target.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setEmail(e.target.value);
|
setEmail(e.target.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleRememberChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleRememberChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setRemember(e.target.checked);
|
setRemember(e.target.checked)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
login({
|
login({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
rememberMe: remember,
|
rememberMe: remember,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"flex h-screen items-center justify-center"}>
|
<div className={"flex h-screen items-center justify-center"}>
|
||||||
@@ -63,7 +63,7 @@ const Login: NextPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Login;
|
export default Login
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import type { FormEvent } from "react";
|
import type { FormEvent } from "react"
|
||||||
import React from "react";
|
import React from "react"
|
||||||
|
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next"
|
||||||
|
|
||||||
import { Button, Input } from "@/components";
|
import { Button, Input } from "@/components"
|
||||||
import { useSignUpMutation } from "@/services";
|
import { useSignUpMutation } from "@/services"
|
||||||
|
|
||||||
const Login: NextPage = () => {
|
const Login: NextPage = () => {
|
||||||
const { mutate: signUp } = useSignUpMutation();
|
const { mutate: signUp } = useSignUpMutation()
|
||||||
|
|
||||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
const formData = new FormData(e.currentTarget);
|
const formData = new FormData(e.currentTarget)
|
||||||
|
|
||||||
const values = Object.fromEntries(formData) as any;
|
const values = Object.fromEntries(formData) as any
|
||||||
signUp(values);
|
signUp(values)
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"flex h-screen items-center justify-center"}>
|
<div className={"flex h-screen items-center justify-center"}>
|
||||||
@@ -43,7 +43,7 @@ const Login: NextPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Login;
|
export default Login
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from "./todolist-api/auth";
|
export * from "./todolist-api/auth"
|
||||||
export * from "./todolist-api/todolists";
|
export * from "./todolist-api/todolists"
|
||||||
|
|||||||
@@ -4,37 +4,37 @@ import type {
|
|||||||
MeResponse,
|
MeResponse,
|
||||||
PostLoginArgs,
|
PostLoginArgs,
|
||||||
PostSignUpArgs,
|
PostSignUpArgs,
|
||||||
} from "../todolists";
|
} from "../todolists"
|
||||||
|
|
||||||
import { handleError } from "@/helpers";
|
import { handleError } from "@/helpers"
|
||||||
import { todolistApiInstance } from "@/services/todolist-api/todolist-api.instance";
|
import { todolistApiInstance } from "@/services/todolist-api/todolist-api.instance"
|
||||||
|
|
||||||
export const AuthApi = {
|
export const AuthApi = {
|
||||||
async login(args: PostLoginArgs) {
|
async login(args: PostLoginArgs) {
|
||||||
const res = await todolistApiInstance.post<LoginResponse>(
|
const res = await todolistApiInstance.post<LoginResponse>(
|
||||||
"/auth/login",
|
"/auth/login",
|
||||||
args,
|
args,
|
||||||
);
|
)
|
||||||
|
|
||||||
localStorage.setItem("accessToken", res.data.accessToken);
|
localStorage.setItem("accessToken", res.data.accessToken)
|
||||||
localStorage.setItem("refreshToken", res.data.refreshToken);
|
localStorage.setItem("refreshToken", res.data.refreshToken)
|
||||||
|
|
||||||
return res.data;
|
return res.data
|
||||||
},
|
},
|
||||||
async signUp(args: PostSignUpArgs) {
|
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() {
|
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() {
|
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)
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router"
|
||||||
|
|
||||||
import { QUERY_KEYS, ROUTES } from "@/constants";
|
import { QUERY_KEYS, ROUTES } from "@/constants"
|
||||||
import { noRefetch } from "@/helpers";
|
import { noRefetch } from "@/helpers"
|
||||||
import { AuthApi } from "@/services/todolist-api/auth/auth.api";
|
import { AuthApi } from "@/services/todolist-api/auth/auth.api"
|
||||||
|
|
||||||
export const useMeQuery = () => {
|
export const useMeQuery = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryFn: AuthApi.me,
|
queryFn: AuthApi.me,
|
||||||
queryKey: [QUERY_KEYS.ME],
|
queryKey: [QUERY_KEYS.ME],
|
||||||
...noRefetch,
|
...noRefetch,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useLoginMutation = () => {
|
export const useLoginMutation = () => {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: AuthApi.login,
|
mutationFn: AuthApi.login,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await queryClient.invalidateQueries([QUERY_KEYS.ME]);
|
await queryClient.invalidateQueries([QUERY_KEYS.ME])
|
||||||
await router.push(ROUTES.HOME);
|
await router.push(ROUTES.HOME)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useLogoutMutation = () => {
|
export const useLogoutMutation = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient()
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: AuthApi.logout,
|
mutationFn: AuthApi.logout,
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await queryClient.invalidateQueries([QUERY_KEYS.ME]);
|
await queryClient.invalidateQueries([QUERY_KEYS.ME])
|
||||||
await router.push(ROUTES.LOGIN);
|
await router.push(ROUTES.LOGIN)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useSignUpMutation = () => {
|
export const useSignUpMutation = () => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: AuthApi.signUp,
|
mutationFn: AuthApi.signUp,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from "./auth.api";
|
export * from "./auth.api"
|
||||||
export * from "./auth.hooks";
|
export * from "./auth.hooks"
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { Mutex } from "async-mutex";
|
import { Mutex } from "async-mutex"
|
||||||
import axios from "axios";
|
import axios from "axios"
|
||||||
import router from "next/router";
|
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({
|
export const todolistApiInstance = axios.create({
|
||||||
baseURL: "http://localhost:3000",
|
baseURL: "http://localhost:3000",
|
||||||
});
|
})
|
||||||
|
|
||||||
async function refreshAccessToken(): Promise<string> {
|
async function refreshAccessToken(): Promise<string> {
|
||||||
const res = await todolistApiInstance.post<{
|
const res = await todolistApiInstance.post<{
|
||||||
refreshToken: string;
|
refreshToken: string
|
||||||
accessToken: string;
|
accessToken: string
|
||||||
}>(
|
}>(
|
||||||
"/auth/refresh-token",
|
"/auth/refresh-token",
|
||||||
{},
|
{},
|
||||||
@@ -22,76 +22,76 @@ async function refreshAccessToken(): Promise<string> {
|
|||||||
Authorization: `Bearer ${localStorage.getItem("refreshToken")}`,
|
Authorization: `Bearer ${localStorage.getItem("refreshToken")}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
localStorage.setItem("accessToken", res.data.accessToken);
|
localStorage.setItem("accessToken", res.data.accessToken)
|
||||||
localStorage.setItem("refreshToken", res.data.refreshToken);
|
localStorage.setItem("refreshToken", res.data.refreshToken)
|
||||||
|
|
||||||
return res.data.accessToken;
|
return res.data.accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
todolistApiInstance.interceptors.request.use(
|
todolistApiInstance.interceptors.request.use(
|
||||||
async (config) => {
|
async (config) => {
|
||||||
if (!config?.url?.includes("refresh-token")) {
|
if (!config?.url?.includes("refresh-token")) {
|
||||||
await mutex.waitForUnlock();
|
await mutex.waitForUnlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
config.headers.Authorization =
|
config.headers.Authorization =
|
||||||
config.headers.Authorization ??
|
config.headers.Authorization ??
|
||||||
`Bearer ${localStorage.getItem("accessToken")}`;
|
`Bearer ${localStorage.getItem("accessToken")}`
|
||||||
|
|
||||||
return config;
|
return config
|
||||||
},
|
},
|
||||||
(error) => Promise.reject(error),
|
(error) => Promise.reject(error),
|
||||||
);
|
)
|
||||||
|
|
||||||
todolistApiInstance.interceptors.response.use(
|
todolistApiInstance.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
async (error) => {
|
async (error) => {
|
||||||
if (error?.response?.request?.responseURL?.includes("refresh-token")) {
|
if (error?.response?.request?.responseURL?.includes("refresh-token")) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
await mutex.waitForUnlock();
|
await mutex.waitForUnlock()
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config
|
||||||
// Check for a 401 response and if this request hasn't been retried yet
|
// Check for a 401 response and if this request hasn't been retried yet
|
||||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
if (!mutex.isLocked()) {
|
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
|
// 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 (refreshedAt && refreshedAt + 60000 > new Date().getTime()) {
|
||||||
// If the token has been refreshed within the last minute, use the refreshed token
|
// If the token has been refreshed within the last minute, use the refreshed token
|
||||||
originalRequest.headers.Authorization = `Bearer ${localStorage.getItem("accessToken")}`;
|
originalRequest.headers.Authorization = `Bearer ${localStorage.getItem("accessToken")}`
|
||||||
release();
|
release()
|
||||||
|
|
||||||
return todolistApiInstance(originalRequest);
|
return todolistApiInstance(originalRequest)
|
||||||
}
|
}
|
||||||
refreshedAt = new Date().getTime();
|
refreshedAt = new Date().getTime()
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
console.log(window.location.pathname);
|
console.log(window.location.pathname)
|
||||||
if (!["/login", "/sign-up"].includes(window.location.pathname)) {
|
if (!["/login", "/sign-up"].includes(window.location.pathname)) {
|
||||||
router.push("/login");
|
router.push("/login")
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
release();
|
release()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await mutex.waitForUnlock();
|
await mutex.waitForUnlock()
|
||||||
const newAccessToken = localStorage.getItem("accessToken");
|
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)
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from "./todolists.hooks";
|
export * from "./todolists.hooks"
|
||||||
export * from "./todolists.api";
|
export * from "./todolists.api"
|
||||||
export * from "./todolists.types";
|
export * from "./todolists.types"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { handleError } from "@/helpers";
|
import { handleError } from "@/helpers"
|
||||||
import type {
|
import type {
|
||||||
CreateTaskResponse,
|
CreateTaskResponse,
|
||||||
CreateTodolistResponse,
|
CreateTodolistResponse,
|
||||||
@@ -8,14 +8,14 @@ import type {
|
|||||||
TasksResponse,
|
TasksResponse,
|
||||||
Todolist,
|
Todolist,
|
||||||
UpdateTaskResponse,
|
UpdateTaskResponse,
|
||||||
} from "@/services";
|
} from "@/services"
|
||||||
import { todolistApiInstance } from "@/services/todolist-api/todolist-api.instance";
|
import { todolistApiInstance } from "@/services/todolist-api/todolist-api.instance"
|
||||||
|
|
||||||
export const TodolistAPI = {
|
export const TodolistAPI = {
|
||||||
async getTodolists() {
|
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 }) {
|
async createTodolist({ title }: { title: string }) {
|
||||||
@@ -23,64 +23,64 @@ export const TodolistAPI = {
|
|||||||
"/todolists",
|
"/todolists",
|
||||||
{
|
{
|
||||||
title,
|
title,
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
|
|
||||||
return handleError(res.data);
|
return handleError(res.data)
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteTodolist({ todolistId }: { todolistId: string }) {
|
async deleteTodolist({ todolistId }: { todolistId: string }) {
|
||||||
const res = await todolistApiInstance.delete<DeleteTodolistResponse>(
|
const res = await todolistApiInstance.delete<DeleteTodolistResponse>(
|
||||||
`/todolists/${todolistId}`
|
`/todolists/${todolistId}`,
|
||||||
);
|
)
|
||||||
|
|
||||||
return res.data;
|
return res.data
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTodolistTasks({ todolistId }: { todolistId: string }) {
|
async getTodolistTasks({ todolistId }: { todolistId: string }) {
|
||||||
const res = await todolistApiInstance.get<TasksResponse>(
|
const res = await todolistApiInstance.get<TasksResponse>(
|
||||||
`/todolists/${todolistId}/tasks`
|
`/todolists/${todolistId}/tasks`,
|
||||||
);
|
)
|
||||||
|
|
||||||
return res.data;
|
return res.data
|
||||||
},
|
},
|
||||||
|
|
||||||
async createTask({
|
async createTask({
|
||||||
todolistId,
|
todolistId,
|
||||||
title,
|
title,
|
||||||
}: {
|
}: {
|
||||||
todolistId: string;
|
todolistId: string
|
||||||
title: string;
|
title: string
|
||||||
}) {
|
}) {
|
||||||
const res = await todolistApiInstance.post<CreateTaskResponse>(
|
const res = await todolistApiInstance.post<CreateTaskResponse>(
|
||||||
`/todolists/${todolistId}/tasks`,
|
`/todolists/${todolistId}/tasks`,
|
||||||
{ title }
|
{ title },
|
||||||
);
|
)
|
||||||
|
|
||||||
return res.data;
|
return res.data
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateTask({ todolistId, task }: { todolistId: string; task: Task }) {
|
async updateTask({ todolistId, task }: { todolistId: string; task: Task }) {
|
||||||
const { id, ...rest } = task;
|
const { id, ...rest } = task
|
||||||
const res = await todolistApiInstance.patch<UpdateTaskResponse>(
|
const res = await todolistApiInstance.patch<UpdateTaskResponse>(
|
||||||
`/todolists/${todolistId}/tasks/${id}`,
|
`/todolists/${todolistId}/tasks/${id}`,
|
||||||
rest
|
rest,
|
||||||
);
|
)
|
||||||
|
|
||||||
return res.data;
|
return res.data
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteTask({
|
async deleteTask({
|
||||||
todolistId,
|
todolistId,
|
||||||
taskId,
|
taskId,
|
||||||
}: {
|
}: {
|
||||||
todolistId: string;
|
todolistId: string
|
||||||
taskId: string;
|
taskId: string
|
||||||
}) {
|
}) {
|
||||||
const res = await todolistApiInstance.delete<DeleteTaskResponse>(
|
const res = await todolistApiInstance.delete<DeleteTaskResponse>(
|
||||||
`/todolists/${todolistId}/tasks/${taskId}`
|
`/todolists/${todolistId}/tasks/${taskId}`,
|
||||||
);
|
)
|
||||||
|
|
||||||
return res.data;
|
return res.data
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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 { QUERY_KEYS } from "@/constants"
|
||||||
import { noRefetch } from "@/helpers";
|
import { noRefetch } from "@/helpers"
|
||||||
import { TodolistAPI } from "@/services";
|
import { TodolistAPI } from "@/services"
|
||||||
|
|
||||||
export const useTodolistsQuery = () => {
|
export const useTodolistsQuery = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryFn: TodolistAPI.getTodolists,
|
queryFn: TodolistAPI.getTodolists,
|
||||||
queryKey: [QUERY_KEYS.TODOLISTS],
|
queryKey: [QUERY_KEYS.TODOLISTS],
|
||||||
...noRefetch,
|
...noRefetch,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useCreateTodolistMutation = () => {
|
export const useCreateTodolistMutation = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: TodolistAPI.createTodolist,
|
mutationFn: TodolistAPI.createTodolist,
|
||||||
//todo: add onMutate
|
//todo: add onMutate
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await queryClient.invalidateQueries([QUERY_KEYS.TODOLISTS]);
|
await queryClient.invalidateQueries([QUERY_KEYS.TODOLISTS])
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useDeleteTodolistMutation = () => {
|
export const useDeleteTodolistMutation = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: TodolistAPI.deleteTodolist,
|
mutationFn: TodolistAPI.deleteTodolist,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries([QUERY_KEYS.TODOLISTS]);
|
queryClient.invalidateQueries([QUERY_KEYS.TODOLISTS])
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useGetTasksQuery = (todolistId: string) => {
|
export const useGetTasksQuery = (todolistId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [QUERY_KEYS.TASKS, todolistId],
|
queryKey: [QUERY_KEYS.TASKS, todolistId],
|
||||||
queryFn: () => TodolistAPI.getTodolistTasks({ todolistId }),
|
queryFn: () => TodolistAPI.getTodolistTasks({ todolistId }),
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useCreateTaskMutation = () => {
|
export const useCreateTaskMutation = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: TodolistAPI.createTask,
|
mutationFn: TodolistAPI.createTask,
|
||||||
onSuccess: (res) => {
|
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 = () => {
|
export const useUpdateTaskMutation = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: TodolistAPI.updateTask,
|
mutationFn: TodolistAPI.updateTask,
|
||||||
onSuccess: async (_, { todolistId }) => {
|
onSuccess: async (_, { todolistId }) => {
|
||||||
await queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId]);
|
await queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId])
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const useDeleteTaskMutation = () => {
|
export const useDeleteTaskMutation = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: TodolistAPI.deleteTask,
|
mutationFn: TodolistAPI.deleteTask,
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
const todolistId = variables.todolistId;
|
const todolistId = variables.todolistId
|
||||||
|
|
||||||
queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId]);
|
queryClient.invalidateQueries([QUERY_KEYS.TASKS, todolistId])
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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 = {
|
export type Task = {
|
||||||
order: string;
|
order: string
|
||||||
title: string;
|
title: string
|
||||||
description?: string | null;
|
description?: string | null
|
||||||
deadline?: string | null;
|
deadline?: string | null
|
||||||
status: TaskStatus;
|
status: TaskStatus
|
||||||
priority: TaskPriority;
|
priority: TaskPriority
|
||||||
ownerId: string;
|
ownerId: string
|
||||||
todoListId: string;
|
todoListId: string
|
||||||
id: string;
|
id: string
|
||||||
createdAt: string;
|
createdAt: string
|
||||||
updatedAt: string;
|
updatedAt: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type Todolist = {
|
export type Todolist = {
|
||||||
id: string;
|
id: string
|
||||||
title: string;
|
title: string
|
||||||
addedDate: Date;
|
addedDate: Date
|
||||||
order: number;
|
order: number
|
||||||
};
|
}
|
||||||
|
|
||||||
export type PostLoginArgs = {
|
export type PostLoginArgs = {
|
||||||
email: string;
|
email: string
|
||||||
password: string;
|
password: string
|
||||||
rememberMe: boolean;
|
rememberMe: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
export type PostSignUpArgs = {
|
export type PostSignUpArgs = {
|
||||||
email: string;
|
email: string
|
||||||
password: string;
|
password: string
|
||||||
username?: string;
|
username?: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type LoginResponseData = {
|
export type LoginResponseData = {
|
||||||
accessToken: string;
|
accessToken: string
|
||||||
refreshToken: string;
|
refreshToken: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type MeResponseData = {
|
export type MeResponseData = {
|
||||||
id: number;
|
id: number
|
||||||
login: string;
|
login: string
|
||||||
email: string;
|
email: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type LoginResponse = LoginResponseData;
|
export type LoginResponse = LoginResponseData
|
||||||
export type LogoutResponse = ApiResponse<never>;
|
export type LogoutResponse = ApiResponse<never>
|
||||||
export type MeResponse = ApiResponse<MeResponseData>;
|
export type MeResponse = ApiResponse<MeResponseData>
|
||||||
|
|
||||||
export type DeleteTodolistResponse = ApiResponse<never>;
|
export type DeleteTodolistResponse = ApiResponse<never>
|
||||||
export type CreateTodolistResponse = ApiResponse<CreateTodolistResponseData>;
|
export type CreateTodolistResponse = ApiResponse<CreateTodolistResponseData>
|
||||||
export type CreateTodolistResponseData = {
|
export type CreateTodolistResponseData = {
|
||||||
item: Todolist;
|
item: Todolist
|
||||||
};
|
}
|
||||||
|
|
||||||
export type CreateTaskResponse = Task;
|
export type CreateTaskResponse = Task
|
||||||
export type DeleteTaskResponse = undefined;
|
export type DeleteTaskResponse = undefined
|
||||||
export type UpdateTaskResponse = undefined;
|
export type UpdateTaskResponse = undefined
|
||||||
|
|
||||||
export enum TaskStatus {
|
export enum TaskStatus {
|
||||||
OPEN = "OPEN",
|
OPEN = "OPEN",
|
||||||
|
|||||||
Reference in New Issue
Block a user