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 "recommended": true
} }
}, },
"javascript": {
"formatter": {
"semicolons": "asNeeded"
}
},
"formatter": { "indentStyle": "space", "indentWidth": 2 } "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 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}</>
}; }

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"> & { 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}
/> />
); )
}; }

View File

@@ -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>
); )
}; }

View File

@@ -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"

View File

@@ -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}
/> />
); )
}; }

View File

@@ -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>
); )
}; }

View File

@@ -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>
); )
}); })

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 * 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 }

View File

@@ -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"

View File

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

View File

@@ -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
View File

@@ -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
View File

@@ -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
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. * 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 }

View File

@@ -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>

View File

@@ -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))
} }

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> => { 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
}; }

View File

@@ -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"

View File

@@ -5,4 +5,4 @@ export const noRefetch = {
refetchOnMount: false, refetchOnMount: false,
refetchIntervalInBackground: false, refetchIntervalInBackground: false,
retry: 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 { 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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)
}, },
}; }

View File

@@ -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,
}); })
}; }

View File

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

View File

@@ -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)
}, },
); )

View File

@@ -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"

View File

@@ -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
}, },
}; }

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 { 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])
}, },
}); })
}; }

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 = { 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",