mirror of
https://github.com/ershisan99/todolist_next.git
synced 2025-12-16 20:59:24 +00:00
refactor ui
This commit is contained in:
6
.idea/git_toolbox_blame.xml
generated
Normal file
6
.idea/git_toolbox_blame.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxBlameSettings">
|
||||
<option name="version" value="2" />
|
||||
</component>
|
||||
</project>
|
||||
17
components.json
Normal file
17
components.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/styles/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
@@ -10,17 +10,21 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"axios": "^1.3.4",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.377.0",
|
||||
"lucide-react": "^0.427.0",
|
||||
"next": "13.2.4",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwind-merge": "^2.5.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useEffect } from "react"
|
||||
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import { Loader } from "../loader"
|
||||
import { Loader } from "../ui/loader"
|
||||
|
||||
import { useMeQuery } from "@/services"
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { ComponentPropsWithoutRef, FC } from "react"
|
||||
|
||||
import { cn } from "@/helpers"
|
||||
|
||||
type Props = ComponentPropsWithoutRef<"button"> & {
|
||||
variant?: "primary" | "outlined" | "icon"
|
||||
}
|
||||
|
||||
export const Button: FC<Props> = ({
|
||||
className,
|
||||
variant = "primary",
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-md border border-gray-300 bg-sky-700 px-4 py-2 text-white focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-600",
|
||||
variant === "outlined" && "border-sky-700 bg-inherit text-sky-700",
|
||||
variant === "icon" &&
|
||||
"h-6 w-6 shrink-0 border-none bg-inherit p-0 text-sky-700 hover:bg-slate-100 hover:text-sky-800",
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Loader } from "../loader"
|
||||
import { Loader } from "../ui/loader"
|
||||
|
||||
export const FullscreenLoader = () => {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from "./todolist"
|
||||
export * from "./auth-redirect"
|
||||
export * from "./button"
|
||||
export * from "./input"
|
||||
export * from "./loader"
|
||||
export * from "./ui/button"
|
||||
export * from "./ui/input"
|
||||
export * from "./ui/loader"
|
||||
export * from "./fullscreen-loader"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { DetailedHTMLProps, FC, InputHTMLAttributes } from "react"
|
||||
|
||||
type Props = DetailedHTMLProps<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
>
|
||||
|
||||
export const Input: FC<Props> = ({ className, ...rest }) => {
|
||||
return (
|
||||
<input
|
||||
className={`w-full rounded-md border border-gray-300 px-4 py-2 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-blue-600 ${className}`}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
40
src/components/mode-toggle.tsx
Normal file
40
src/components/mode-toggle.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import * as React from "react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
9
src/components/theme-provider.tsx
Normal file
9
src/components/theme-provider.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
import type { ThemeProviderProps } from "next-themes/dist/types"
|
||||
import * as React from "react"
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
@@ -1,19 +1,31 @@
|
||||
import type { ChangeEvent, FC, FormEvent } from "react"
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type FC,
|
||||
type FormEvent,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react"
|
||||
import { memo, useState } from "react"
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Plus, Trash } from "lucide-react"
|
||||
|
||||
import type {
|
||||
Task,
|
||||
Todolist as TodolistType,
|
||||
} from "../../services/todolist-api/todolists"
|
||||
import { Button } from "../button"
|
||||
import { Input } from "../input"
|
||||
import { Button } from "../ui/button"
|
||||
import { Input } from "../ui/input"
|
||||
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/components/toggle-group/toggle-group"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
|
||||
import {
|
||||
TaskStatus,
|
||||
useCreateTaskMutation,
|
||||
@@ -31,33 +43,42 @@ type Filter = "all" | "active" | "completed"
|
||||
|
||||
export const Todolist: FC<Props> = memo(({ todolist }) => {
|
||||
const { data: tasks, isLoading } = useGetTasksQuery(todolist.id)
|
||||
const { mutate: putTask } = useUpdateTaskMutation()
|
||||
const { mutate: updateTask } = useUpdateTaskMutation()
|
||||
const { mutate: deleteTask } = useDeleteTaskMutation()
|
||||
const { mutate: deleteTodolist } = useDeleteTodolistMutation()
|
||||
const { mutate: createTask } = useCreateTaskMutation()
|
||||
const [newTaskTitle, setNewTaskTitle] = useState("")
|
||||
const [filter, setFilter] = useState("all")
|
||||
|
||||
const handleChangeStatus = (todolistId: string, task: Task) => {
|
||||
const newTask: Task = {
|
||||
...task,
|
||||
status:
|
||||
task.status === TaskStatus.DONE ? TaskStatus.OPEN : TaskStatus.DONE,
|
||||
}
|
||||
const handleChangeStatus = useCallback(
|
||||
(todolistId: string, task: Task) => {
|
||||
updateTask({
|
||||
todolistId,
|
||||
id: task.id,
|
||||
status:
|
||||
task.status === TaskStatus.DONE ? TaskStatus.OPEN : TaskStatus.DONE,
|
||||
})
|
||||
},
|
||||
[updateTask],
|
||||
)
|
||||
|
||||
const handleDeleteTask = useCallback(
|
||||
(todolistId: string, taskId: string) => {
|
||||
deleteTask({ todolistId, taskId })
|
||||
},
|
||||
[deleteTask],
|
||||
)
|
||||
|
||||
putTask({ todolistId, task: newTask })
|
||||
}
|
||||
const handleDeleteTask = (todolistId: string, taskId: string) => {
|
||||
deleteTask({ todolistId, taskId })
|
||||
}
|
||||
const handleDeleteTodolist = (todolistId: string) => {
|
||||
deleteTodolist({ todolistId })
|
||||
}
|
||||
|
||||
const handleAddTask = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
createTask({ todolistId: todolist.id, title: newTaskTitle })
|
||||
setNewTaskTitle("")
|
||||
}
|
||||
|
||||
const handleNewTaskTitleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setNewTaskTitle(e.target.value)
|
||||
}
|
||||
@@ -66,7 +87,6 @@ export const Todolist: FC<Props> = memo(({ todolist }) => {
|
||||
setFilter(value as Filter)
|
||||
}
|
||||
|
||||
if (isLoading) return <div>loading...</div>
|
||||
const filteredTasks = tasks?.filter((task) => {
|
||||
switch (filter) {
|
||||
case "active":
|
||||
@@ -78,53 +98,57 @@ export const Todolist: FC<Props> = memo(({ todolist }) => {
|
||||
}
|
||||
})
|
||||
|
||||
const renderTasks = useMemo(() => {
|
||||
return filteredTasks?.map((task) => {
|
||||
return (
|
||||
<li className={"flex cursor-pointer items-center"} key={task.id}>
|
||||
<label className={"flex cursor-pointer items-center gap-2"}>
|
||||
<Checkbox
|
||||
onCheckedChange={() => handleChangeStatus(todolist.id, task)}
|
||||
checked={TaskStatus.DONE === task.status}
|
||||
/>
|
||||
{task.title}
|
||||
</label>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
onClick={() => handleDeleteTask(todolist.id, task.id)}
|
||||
className={"ml-2"}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</li>
|
||||
)
|
||||
})
|
||||
}, [filteredTasks, todolist.id, handleChangeStatus, handleDeleteTask])
|
||||
|
||||
return (
|
||||
<div
|
||||
key={todolist.id}
|
||||
className={
|
||||
"flex w-72 flex-col gap-3 rounded-md border border-gray-300 p-4"
|
||||
}
|
||||
>
|
||||
<div className={"flex items-center justify-between "}>
|
||||
<h2 className={"text-xl"}>{todolist.title}</h2>
|
||||
<Button
|
||||
variant={"icon"}
|
||||
onClick={() => handleDeleteTodolist(todolist.id)}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleAddTask}
|
||||
className={"relative flex w-full items-center gap-4"}
|
||||
>
|
||||
<Input value={newTaskTitle} onChange={handleNewTaskTitleChange} />
|
||||
<Button type={"submit"} variant={"icon"} className={"absolute right-4"}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</form>
|
||||
{filteredTasks?.map((task) => {
|
||||
return (
|
||||
<div className={"flex cursor-pointer items-center"} key={task.id}>
|
||||
<label className={"flex cursor-pointer items-center gap-2"}>
|
||||
<input
|
||||
onChange={() => handleChangeStatus(todolist.id, task)}
|
||||
type={"checkbox"}
|
||||
checked={TaskStatus.DONE === task.status}
|
||||
/>
|
||||
{task.title}
|
||||
</label>
|
||||
<Button
|
||||
variant={"icon"}
|
||||
onClick={() => handleDeleteTask(todolist.id, task.id)}
|
||||
className={"ml-2"}
|
||||
>
|
||||
<Trash size={16} />
|
||||
<Card key={todolist.id} className={"w-72 flex flex-col justify-between"}>
|
||||
<div>
|
||||
<CardHeader className={"flex flex-row items-center justify-between"}>
|
||||
<CardTitle>{todolist.title}</CardTitle>
|
||||
<Button
|
||||
variant={"ghost"}
|
||||
size={"icon"}
|
||||
onClick={() => handleDeleteTodolist(todolist.id)}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
onSubmit={handleAddTask}
|
||||
className={"relative flex w-full items-center gap-4"}
|
||||
>
|
||||
<Input value={newTaskTitle} onChange={handleNewTaskTitleChange} />
|
||||
<Button type={"submit"} variant={"ghost"} size={"icon"}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className={"flex"}>
|
||||
</form>
|
||||
<ul className={"mt-4 space-y-2"}>{isLoading ? null : renderTasks}</ul>
|
||||
</CardContent>
|
||||
</div>
|
||||
<CardFooter className={"flex"}>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
onValueChange={handleFilterChange}
|
||||
@@ -132,16 +156,16 @@ export const Todolist: FC<Props> = memo(({ todolist }) => {
|
||||
className={"w-full"}
|
||||
>
|
||||
<ToggleGroupItem className={"w-full"} value="all">
|
||||
All
|
||||
ALL
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem className={"w-full"} value="active">
|
||||
Active
|
||||
TODO
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem className={"w-full"} value="completed">
|
||||
Completed
|
||||
DONE
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
|
||||
56
src/components/ui/button.tsx
Normal file
56
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { type VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10 shrink-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
79
src/components/ui/card.tsx
Normal file
79
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
28
src/components/ui/checkbox.tsx
Normal file
28
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
198
src/components/ui/dropdown-menu.tsx
Normal file
198
src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
))
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
))
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
))
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
))
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
35
src/components/ui/input/index.tsx
Normal file
35
src/components/ui/input/index.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { cn } from "@/helpers"
|
||||
import * as React from "react"
|
||||
import { useId } from "react"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, label, id, ...props }, ref) => {
|
||||
const generatedId = useId()
|
||||
const idToUse = id || generatedId
|
||||
|
||||
return (
|
||||
<div className="grid items-center gap-1.5">
|
||||
{label && <Label htmlFor={idToUse}>{label}</Label>}
|
||||
<input
|
||||
id={idToUse}
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
15
src/components/ui/skeleton.tsx
Normal file
15
src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
@@ -1,34 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||
import type { VariantProps } from "class-variance-authority"
|
||||
import { cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/helpers"
|
||||
import { toggleVariants } from "@/components/ui/toggle"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex border border-sky-700 items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-sky-700 data-[state=on]:text-white",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-3",
|
||||
sm: "h-8 px-2",
|
||||
lg: "h-10 px-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
43
src/components/ui/toggle.tsx
Normal file
43
src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-3",
|
||||
sm: "h-9 px-2.5",
|
||||
lg: "h-11 px-5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toggle = React.forwardRef<
|
||||
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>
|
||||
>(({ className, variant, size, ...props }, ref) => (
|
||||
<TogglePrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import "../styles/globals.css"
|
||||
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query"
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||
import type { AppType } from "next/dist/shared/lib/utils"
|
||||
|
||||
import { AuthRedirect } from "@/components"
|
||||
import { AuthRedirect, Button } from "@/components"
|
||||
import { ModeToggle } from "@/components/mode-toggle"
|
||||
import { ThemeProvider } from "@/components/theme-provider"
|
||||
import { useLogoutMutation } from "@/services"
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
@@ -11,10 +14,36 @@ const MyApp: AppType = ({ Component, pageProps }) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthRedirect>
|
||||
<Component {...pageProps} />
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<Header />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</AuthRedirect>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function Header() {
|
||||
const { mutate: logout } = useLogoutMutation()
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
}
|
||||
return (
|
||||
<header className={"flex justify-between items-center p-4"}>
|
||||
<h1 className={"text-3xl"}>Tasks</h1>
|
||||
<div className={"flex gap-4"}>
|
||||
<ModeToggle />
|
||||
<Button onClick={handleLogout} variant={"outline"}>
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
||||
|
||||
13
src/pages/_document.tsx
Normal file
13
src/pages/_document.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Head, Html, Main, NextScript } from "next/document"
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en" className={"bg-zinc-950 h-screen"}>
|
||||
<Head />
|
||||
<body className={"h-full grid grid-rows-[auto_1fr] overflow-hidden"}>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +1,17 @@
|
||||
import type { ChangeEvent, FormEvent } from "react"
|
||||
import { type ChangeEvent, type FormEvent, useMemo } from "react"
|
||||
import { useState } from "react"
|
||||
|
||||
import type { NextPage } from "next"
|
||||
import Head from "next/head"
|
||||
|
||||
import { Todolist, Button, FullscreenLoader, Input } from "@/components"
|
||||
import {
|
||||
useCreateTodolistMutation,
|
||||
useLogoutMutation,
|
||||
useTodolistsQuery,
|
||||
} from "@/services"
|
||||
import { Button, FullscreenLoader, Input, Todolist } from "@/components"
|
||||
import { useCreateTodolistMutation, useTodolistsQuery } from "@/services"
|
||||
import { Plus } from "lucide-react"
|
||||
|
||||
const Home: NextPage = () => {
|
||||
const [newTodolistTitle, setNewTodolistTitle] = useState("")
|
||||
const { mutate: logout } = useLogoutMutation()
|
||||
const { data: todolists, isLoading: isTodolistsLoading } = useTodolistsQuery()
|
||||
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
}
|
||||
|
||||
const { mutate: createTodolist } = useCreateTodolistMutation()
|
||||
|
||||
const handleAddTodolist = (e: FormEvent<HTMLFormElement>) => {
|
||||
@@ -32,6 +24,12 @@ const Home: NextPage = () => {
|
||||
setNewTodolistTitle(e.target.value)
|
||||
}
|
||||
|
||||
const renderTodolists = useMemo(() => {
|
||||
return todolists?.map((todolist) => {
|
||||
return <Todolist todolist={todolist} key={todolist.id} />
|
||||
})
|
||||
}, [todolists])
|
||||
|
||||
if (isTodolistsLoading) return <FullscreenLoader />
|
||||
|
||||
return (
|
||||
@@ -41,26 +39,19 @@ const Home: NextPage = () => {
|
||||
<meta name="description" content="Incubator todolist" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<header className={"flex justify-between p-4"}>
|
||||
<h1 className={"text-3xl"}>Todolist</h1>
|
||||
<Button onClick={handleLogout}>Logout</Button>
|
||||
</header>
|
||||
<main className={"flex flex-col gap-4 p-4"}>
|
||||
<form onSubmit={handleAddTodolist} className={"flex items-end gap-2.5"}>
|
||||
<label className={"flex w-52 flex-col gap-0.5"}>
|
||||
new todolist
|
||||
<Input
|
||||
value={newTodolistTitle}
|
||||
onChange={handleNewTodolistTitleChange}
|
||||
/>
|
||||
</label>
|
||||
<Button type={"submit"}>+</Button>
|
||||
<Input
|
||||
className={"w-52"}
|
||||
label={"New list"}
|
||||
value={newTodolistTitle}
|
||||
onChange={handleNewTodolistTitleChange}
|
||||
/>
|
||||
<Button type={"submit"} variant={"outline"} size={"icon"}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</form>
|
||||
<div className={"flex flex-wrap gap-4"}>
|
||||
{todolists?.map((todolist) => {
|
||||
return <Todolist todolist={todolist} key={todolist.id} />
|
||||
})}
|
||||
</div>
|
||||
<div className={"flex flex-wrap gap-4"}>{renderTodolists}</div>
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useState } from "react"
|
||||
import type { NextPage } from "next"
|
||||
|
||||
import { Button, Input } from "@/components"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { useLoginMutation } from "@/services"
|
||||
|
||||
const Login: NextPage = () => {
|
||||
@@ -34,30 +35,30 @@ const Login: NextPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"flex h-screen items-center justify-center"}>
|
||||
<div className={"flex h-full items-center justify-center"}>
|
||||
<div className={"flex w-52 flex-col gap-3"}>
|
||||
<label className={"flex flex-col gap-1"}>
|
||||
Email
|
||||
<Input value={email} onChange={handleEmailChange} type="email" />
|
||||
</label>
|
||||
<Input
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
type="email"
|
||||
label={"Email"}
|
||||
/>
|
||||
|
||||
<label className={"flex flex-col gap-1"}>
|
||||
Password
|
||||
<Input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
</label>
|
||||
<Input
|
||||
type="password"
|
||||
label={"Password"}
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
|
||||
<label className={"flex items-center gap-2"}>
|
||||
<Label className={"flex items-center gap-2"}>
|
||||
<input
|
||||
type={"checkbox"}
|
||||
checked={remember}
|
||||
onChange={handleRememberChange}
|
||||
/>
|
||||
Remember me
|
||||
</label>
|
||||
</Label>
|
||||
<Button className={"w-full"} onClick={handleSubmit}>
|
||||
Login
|
||||
</Button>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const TodolistAPI = {
|
||||
},
|
||||
)
|
||||
|
||||
return handleError(res.data)
|
||||
return res.data
|
||||
},
|
||||
|
||||
async deleteTodolist({ todolistId }: { todolistId: string }) {
|
||||
@@ -60,8 +60,11 @@ export const TodolistAPI = {
|
||||
return res.data
|
||||
},
|
||||
|
||||
async updateTask({ todolistId, task }: { todolistId: string; task: Task }) {
|
||||
const { id, ...rest } = task
|
||||
async updateTask({
|
||||
id,
|
||||
todolistId,
|
||||
...rest
|
||||
}: Partial<Task> & Required<Pick<Task, "id">> & { todolistId: string }) {
|
||||
const res = await todolistApiInstance.patch<UpdateTaskResponse>(
|
||||
`/todolists/${todolistId}/tasks/${id}`,
|
||||
rest,
|
||||
|
||||
@@ -17,9 +17,8 @@ export const useCreateTodolistMutation = () => {
|
||||
|
||||
return useMutation({
|
||||
mutationFn: TodolistAPI.createTodolist,
|
||||
//todo: add onMutate
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries([QUERY_KEYS.TODOLISTS])
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries([QUERY_KEYS.TODOLISTS])
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,48 +1,72 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
.loader {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px dotted #000;
|
||||
border-style: solid solid dotted dotted;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 2s linear infinite;
|
||||
}
|
||||
.loader::after {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
border: 3px dotted #ff3d00;
|
||||
border-style: solid solid dotted;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
animation: rotationBack 1s linear infinite;
|
||||
transform-origin: center center;
|
||||
|
||||
@layer base {
|
||||
#__next{
|
||||
height: 100%;
|
||||
}
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes rotationBack {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
80
tailwind.config.ts
Normal file
80
tailwind.config.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { Config } from "tailwindcss"
|
||||
|
||||
const config = {
|
||||
darkMode: ["class"],
|
||||
content: [
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
} satisfies Config
|
||||
|
||||
export default config
|
||||
Reference in New Issue
Block a user