Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/fcc.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React + TS</title>
<title>DMMO</title>
</head>
<body>
<div id="root"></div>
Expand Down
8 changes: 5 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Toaster } from 'react-hot-toast'

function App() {
return (
<div className="flex items-center justify-center h-screen">
<h1 className="text-5xl font-bold">Todo App Tutorial</h1>
<div>
<Toaster position="top-right" />
<AddTodo />
<TodoList />
</div>
)
}

export default App
export default App
46 changes: 43 additions & 3 deletions src/components/AddTodo.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,48 @@

import React, { useEffect, useRef, useState } from 'react'
import { toast } from 'react-hot-toast'
import { useTodo } from '../context'
import { useTodo } from '../context/useTodo'
import { Input } from './Input'

export const AddTodo = () => {
return <div>Add To Do</div>
}
const [input, setInput] = useState<string>('');
// const [todos, setTodos] = useState<string[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
const {addTodo} = useTodo();
const handleSubmit=(e:React.FormEvent)=>{
e.preventDefault();
if(input.trim()!==''){
addTodo(input);
setInput('');
toast.success('Todo added successfully');
}else {
toast.error('Todo field cannot be empty!');
}
// setTodos([...todos, input]);
}
useEffect(()=>{
if(inputRef.current){
inputRef.current.focus();
}
},[]);
return (
<form onSubmit={handleSubmit}>
<div className="flex items-center w-full max-w-lg gap-2 p-5 m-auto">
<Input
value={input}
ref={inputRef}
onChange={e => setInput(e.target.value)}
type="text"
className="w-full px-5 py-2 bg-transparent border-2 outline-none border-zinc-600 rounded-xl placeholder:text-zinc-500 focus:border-white"
placeholder="start typing ..."
/>
<button
type="submit"
className="px-5 py-2 text-sm font-normal text-blue-300 bg-blue-900 border-2 border-blue-900 active:scale-95 rounded-xl"
>
Submit
</button>
</div>
</form>
)
}
15 changes: 12 additions & 3 deletions src/components/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import cn from 'classnames'

import React from 'react'

export const Input = () => {
return <div>Input</div>
}
export const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>>(({className, ...rest}, ref) => {
return (
<input
{...rest}
ref={ref}
className={cn(
'w-full px-5 py-2 bg-transparent border-2 outline-none border-zinc-600 rounded-xl placeholder:text-zinc-500 focus:border-white',
className,
)}
/>
)
});
122 changes: 118 additions & 4 deletions src/components/TodoItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Todo } from '../context'
import { useEffect, useRef, useState } from 'react'
import { useTodo } from '../context'
import { Todo } from '../context/TodoContext'
import { useTodo } from '../context/useTodo'
import { Input } from './Input'
import { BsCheck2Square } from 'react-icons/bs'
import { TbRefresh } from 'react-icons/tb'
Expand All @@ -11,5 +11,119 @@ import cn from 'classnames'
import { motion } from 'framer-motion'

export const TodoItem = (props: { todo: Todo }) => {
return <div>Todo Item</div>
}
const { todo } = props

const [editingTodoText, setEditingTodoText] = useState<string>('')
const [editingTodoId, setEditingTodoId] = useState<string | null>(null)

const { deleteTodo, editTodo, updateTodoStatus } = useTodo()

const editInputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
if (editingTodoId !== null && editInputRef.current) {
editInputRef.current.focus()
}
}, [editingTodoId])

const handleEdit = (todoId: string, todoText: string) => {
setEditingTodoId(todoId)
setEditingTodoText(todoText)

if (editInputRef.current) {
editInputRef.current.focus()
}
}

const handleUpdate = (todoId: string) => {
if (editingTodoText.trim() !== '') {
editTodo(todoId, editingTodoText)
setEditingTodoId(null)
setEditingTodoText('')
toast.success('Todo updated successfully!')
} else {
toast.error('Todo field cannot be empty!')
}
}

const handleDelete = (todoId: string) => {
deleteTodo(todoId)
toast.success('Todo deleted successfully!')
}

const handleStatusUpdate = (todoId: string) => {
updateTodoStatus(todoId)
toast.success('Todo status updated successfully!')
}

return (
<motion.li
layout
key={todo.id}
className={cn(
'p-5 rounded-xl bg-zinc-900',
todo.status === 'completed' && 'bg-opacity-50 text-zinc-500',
)}
>
{editingTodoId === todo.id ? (
<motion.div layout className="flex gap-2">
<Input
ref={editInputRef}
type="text"
value={editingTodoText}
onChange={e => setEditingTodoText(e.target.value)}
/>
<button
className="px-5 py-2 text-sm font-normal text-orange-300 bg-orange-900 border-2 border-orange-900 active:scale-95 rounded-xl"
onClick={() => handleUpdate(todo.id)}
>
Update
</button>
</motion.div>
) : (
<div className="flex flex-col gap-5">
<motion.span
layout
style={{
textDecoration:
todo.status === 'completed' ? 'line-through' : 'none',
}}
>
{todo.text}
</motion.span>
<div className="flex justify-between gap-5 text-white">
<button onClick={() => handleStatusUpdate(todo.id)}>
{todo.status === 'undone' ? (
<span className="flex items-center gap-1">
<BsCheck2Square />
Mark Completed
</span>
) : (
<span className="flex items-center gap-1">
<TbRefresh />
Mark Undone
</span>
)}
</button>
<div className="flex items-center gap-2">
<button
onClick={() => handleEdit(todo.id, todo.text)}
className="flex items-center gap-1 "
>
<FaRegEdit />
Edit
</button>
<button
onClick={() => handleDelete(todo.id)}
className="flex items-center gap-1 text-red-500"
>
<RiDeleteBin7Line />
Delete
</button>
</div>
</div>
</div>
)}
</motion.li>
)
}
20 changes: 19 additions & 1 deletion src/components/TodoList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { TodoItem } from './TodoItem'
import { useTodo } from '../context'
import { SiStarship } from 'react-icons/si'
import { motion } from 'framer-motion'

export const TodoList = () => {
return <div>TodoList</div>
const {todos} = useTodo();
if(!todos.length){
return (
<div className="max-w-lg px-5 m-auto">
<h1 className="flex flex-col items-center gap-5 px-5 py-10 text-xl font-bold text-center rounded-xl bg-zinc-900">
<SiStarship className="text-5xl" />
You have nothing to do!
</h1>
</div>
)
}
return (
<motion.ul className="grid max-w-lg gap-2 px-5 m-auto">
{[...todos.filter(todo => todo.status === 'undone'), ...todos.filter(todo => todo.status === 'completed')].map(todo => (
<TodoItem todo={todo} key={todo.id} />
))}
</motion.ul>
)
}
40 changes: 36 additions & 4 deletions src/context/TodoContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,45 @@ export interface Todo {
text: string
status: 'undone' | 'completed'
}
interface TodoContextProps {
todos: Todo[]
addTodo: (text: string) => void
deleteTodo:(id: string) => void
editTodo:(id: string, text: string) => void
updateTodoStatus:(id:string)=>void

export const TodoContext = createContext<undefined>(undefined)
}
export const TodoContext = createContext<TodoContextProps | undefined>(
undefined,
)

export const TodoProvider = (props: { children: React.ReactNode }) => {
const [todos, setTodos] = useState<Todo[]>([])
const addTodo = (text: string) => {
const newTodo: Todo = {
id: nanoid(),
text,
status: 'undone',
}
setTodos([...todos, newTodo])
}
const deleteTodo = (id: string) => {
setTodos(todos.filter(todo => todo.id !== id))
}
const editTodo = (id: string, text: string) => {
setTodos(prevTodos=>prevTodos.map(todo=>todo.id===id?{...todo,text}:todo))
}
const updateTodoStatus = (id: string) => {
setTodos(prevTodos=>prevTodos.map(todo=>todo.id===id?{...todo,status:todo.status==='undone'?'completed':'undone'}:todo))
}
const value: TodoContextProps = {
todos,
addTodo,
deleteTodo,
editTodo,
updateTodoStatus,
}
return (
<TodoContext.Provider value={undefined}>
{props.children}
</TodoContext.Provider>
<TodoContext.Provider value={value}>{props.children}</TodoContext.Provider>
)
}
4 changes: 3 additions & 1 deletion src/context/useTodo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ import { useContext } from 'react'
import { TodoContext } from './TodoContext'

export const useTodo = () => {
return
const context = useContext(TodoContext)
if (!context) throw new Error('useTodo must be used within a TodoProvider')
return context
}
4 changes: 3 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { TodoProvider } from './context'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
<TodoProvider>
<App />
</TodoProvider>
</React.StrictMode>,
)