├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── fcc.png ├── thumbnail.jpg └── vite.svg ├── src ├── App.tsx ├── assets │ └── react.svg ├── components │ ├── AddTodo.tsx │ ├── Input.tsx │ ├── TodoItem.tsx │ ├── TodoList.tsx │ └── index.ts ├── context │ ├── TodoContext.tsx │ ├── index.ts │ └── useTodo.ts ├── index.css ├── main.tsx └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | build 4 | src/globe/countries-110m.json 5 | src/us-cities.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "endOfLine": "lf", 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": false, 8 | "printWidth": 80, 9 | "proseWrap": "always", 10 | "quoteProps": "as-needed", 11 | "requirePragma": false, 12 | "semi": false, 13 | "singleQuote": true, 14 | "tabWidth": 2, 15 | "trailingComma": "all", 16 | "useTabs": false 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 | Logo 8 | 9 | 10 |

TypeScript Handbook for React Developers – How to Build a Type-Safe Todo App

11 | 12 |

13 | A Step-By-Step Tutorial for Beginners 14 |
15 | Read on FreeCodeCamp » 16 |
17 |
18 | View Demo 19 | · 20 | Request Feature 21 | · 22 | Report Bug 23 |

24 | 25 | [![freeCodeCamp](https://img.shields.io/badge/-freeCodeCamp-brightgreen?logo=freeCodeCamp)](https://www.freecodecamp.org/) 26 | [![React](https://img.shields.io/badge/-React-blue?logo=React)](https://reactjs.org/) 27 | [![TypeScript](https://img.shields.io/badge/-TypeScript-6E36F6?logo=TypeScript&logoColor=white&color=black)](#) 28 | [![Tailwind CSS](https://img.shields.io/badge/-Tailwind%20CSS-06B6D4?logo=Tailwind%20CSS&logoColor=black&color=white)](https://tailwindcss.com/) 29 | [![Framer Motion](https://img.shields.io/badge/-Framer%20Motion-blue?logo=Framer)](https://www.framer.com/api/motion/) 30 | 31 |
32 | 33 |
34 | 35 | ![Thumbnail](./public/thumbnail.jpg) 36 | 37 |
38 | 39 | ## Introduction 40 | 41 | Welcome to the GitHub repository for the 42 | [TypeScript for React Developers](https://www.freecodecamp.org/news/typescript-tutorial-for-react-developers) 43 | tutorial! 44 | 45 | In today's JavaScript landscape, TypeScript is gaining more and more popularity, 46 | and React developers are increasingly embracing it. If you're a React developer 47 | looking to explore TypeScript or enhance your skills with it, this tutorial is 48 | just for you. I'll guide you through using TypeScript in a React application by 49 | building a classic todo app. 50 | 51 | ## Getting Started 52 | 53 | To get started with the project in your local development environment, follow 54 | these steps: 55 | 56 | 1. Clone the repository to your local machine. 57 | 58 | ```bash 59 | git@github.com:Yazdun/react-ts-fcc-tutorial.git 60 | ``` 61 | 62 | 2. Open the cloned folder in your preferred code editor, install the required 63 | dependencies by running the following command in the terminal: 64 | 65 | ```bash 66 | npm install 67 | ``` 68 | 69 | 3. To access the starter files for the tutorial, use the following command: 70 | 71 | ```bash 72 | git checkout starter 73 | ``` 74 | 75 | 4. Start the development server by running the following command: 76 | 77 | ```bash 78 | npm run dev 79 | ``` 80 | 81 | You are now ready to go! 82 | 83 | ## Contribute to this project 84 | 85 | Thank you for browsing this repo. Any contributions you make are **greatly 86 | appreciated**. 87 | 88 | If you have a suggestion that would make this better, please fork the repo and 89 | create a pull request. You can also simply open an issue with the tag 90 | "enhancement". Don't forget to give the project a star! Thanks again! 91 | 92 | 1. Fork the Project 93 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 94 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 95 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 96 | 5. Open a Pull Request 97 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ts-fcc-tutorial", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "classnames": "^2.3.2", 14 | "framer-motion": "^10.12.16", 15 | "nanoid": "^4.0.2", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-hot-toast": "^2.4.1", 19 | "react-icons": "^4.9.0", 20 | "usehooks-ts": "^2.9.1" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.0.37", 24 | "@types/react-dom": "^18.0.11", 25 | "@typescript-eslint/eslint-plugin": "^5.59.0", 26 | "@typescript-eslint/parser": "^5.59.0", 27 | "@vitejs/plugin-react": "^4.0.0", 28 | "autoprefixer": "^10.4.14", 29 | "eslint": "^8.38.0", 30 | "eslint-plugin-react-hooks": "^4.6.0", 31 | "eslint-plugin-react-refresh": "^0.3.4", 32 | "postcss": "^8.4.24", 33 | "tailwindcss": "^3.3.2", 34 | "typescript": "^5.0.2", 35 | "vite": "^4.3.9" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/fcc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yazdun/react-ts-fcc-tutorial/fb2c0dcc5da89fcf645a39176a9dd21a99541b9d/public/fcc.png -------------------------------------------------------------------------------- /public/thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yazdun/react-ts-fcc-tutorial/fb2c0dcc5da89fcf645a39176a9dd21a99541b9d/public/thumbnail.jpg -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { TodoList, AddTodo } from './components' 2 | import { Toaster } from 'react-hot-toast' 3 | 4 | function App() { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ) 12 | } 13 | 14 | export default App 15 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AddTodo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react' 2 | import { toast } from 'react-hot-toast' 3 | import { useTodo } from '../context' 4 | import { Input } from './Input' 5 | 6 | export const AddTodo = () => { 7 | const [input, setInput] = useState('') 8 | const inputRef = useRef(null) 9 | const { addTodo } = useTodo() 10 | 11 | useEffect(() => { 12 | if (inputRef.current) { 13 | inputRef.current.focus() 14 | } 15 | }, []) 16 | 17 | const handleSubmission = (e: React.FormEvent) => { 18 | e.preventDefault() 19 | if (input.trim() !== '') { 20 | addTodo(input) 21 | setInput('') 22 | toast.success('Todo added successfully!') 23 | } else { 24 | toast.error('Todo field cannot be empty!') 25 | } 26 | } 27 | 28 | return ( 29 |
30 |
31 | setInput(e.target.value)} 37 | /> 38 | 44 |
45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import { InputHTMLAttributes, forwardRef } from 'react' 2 | import cn from 'classnames' 3 | 4 | export const Input = forwardRef< 5 | HTMLInputElement, 6 | InputHTMLAttributes 7 | >(({ className, ...rest }, ref) => { 8 | return ( 9 | 17 | ) 18 | }) 19 | -------------------------------------------------------------------------------- /src/components/TodoItem.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react' 2 | import { useTodo } from '../context' 3 | import type { Todo } from '../context' 4 | import { Input } from './Input' 5 | import { BsCheck2Square } from 'react-icons/bs' 6 | import { TbRefresh } from 'react-icons/tb' 7 | import { FaRegEdit } from 'react-icons/fa' 8 | import { RiDeleteBin7Line } from 'react-icons/ri' 9 | import { toast } from 'react-hot-toast' 10 | import cn from 'classnames' 11 | import { motion } from 'framer-motion' 12 | 13 | export const TodoItem = (props: { todo: Todo }) => { 14 | const { todo } = props 15 | 16 | const [editingTodoText, setEditingTodoText] = useState('') 17 | const [editingTodoId, setEditingTodoId] = useState(null) 18 | 19 | const { deleteTodo, editTodo, updateTodoStatus } = useTodo() 20 | 21 | const editInputRef = useRef(null) 22 | 23 | useEffect(() => { 24 | if (editingTodoId !== null && editInputRef.current) { 25 | editInputRef.current.focus() 26 | } 27 | }, [editingTodoId]) 28 | 29 | const handleEdit = (todoId: string, todoText: string) => { 30 | setEditingTodoId(todoId) 31 | setEditingTodoText(todoText) 32 | 33 | if (editInputRef.current) { 34 | editInputRef.current.focus() 35 | } 36 | } 37 | 38 | const handleUpdate = (todoId: string) => { 39 | if (editingTodoText.trim() !== '') { 40 | editTodo(todoId, editingTodoText) 41 | setEditingTodoId(null) 42 | setEditingTodoText('') 43 | toast.success('Todo updated successfully!') 44 | } else { 45 | toast.error('Todo field cannot be empty!') 46 | } 47 | } 48 | 49 | const handleDelete = (todoId: string) => { 50 | deleteTodo(todoId) 51 | toast.success('Todo deleted successfully!') 52 | } 53 | 54 | const handleStatusUpdate = (todoId: string) => { 55 | updateTodoStatus(todoId) 56 | toast.success('Todo status updated successfully!') 57 | } 58 | 59 | return ( 60 | 68 | {editingTodoId === todo.id ? ( 69 | 70 | setEditingTodoText(e.target.value)} 75 | /> 76 | 82 | 83 | ) : ( 84 |
85 | 92 | {todo.text} 93 | 94 |
95 | 108 |
109 | 116 | 123 |
124 |
125 |
126 | )} 127 |
128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import { TodoItem } from './TodoItem' 2 | import { useTodo } from '../context' 3 | import { SiStarship } from 'react-icons/si' 4 | 5 | export const TodoList = () => { 6 | const { todos } = useTodo() 7 | 8 | if (!todos.length) { 9 | return ( 10 |
11 |

12 | 13 | You have nothing to do! 14 |

15 |
16 | ) 17 | } 18 | 19 | return ( 20 |
    21 | {todos.map(todo => ( 22 | 23 | ))} 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { AddTodo } from './AddTodo' 2 | export { Input } from './Input' 3 | export { TodoItem } from './TodoItem' 4 | export { TodoList } from './TodoList' 5 | -------------------------------------------------------------------------------- /src/context/TodoContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react' 2 | import { nanoid } from 'nanoid' 3 | import { useLocalStorage } from 'usehooks-ts' 4 | 5 | interface TodoContextProps { 6 | todos: Todo[] 7 | addTodo: (text: string) => void 8 | deleteTodo: (id: string) => void 9 | editTodo: (id: string, text: string) => void 10 | updateTodoStatus: (id: string) => void 11 | } 12 | 13 | export interface Todo { 14 | id: string 15 | text: string 16 | status: 'undone' | 'completed' 17 | } 18 | 19 | export const TodoContext = createContext( 20 | undefined, 21 | ) 22 | 23 | export const TodoProvider = (props: { children: React.ReactNode }) => { 24 | const [todos, setTodos] = useLocalStorage('todos', []) 25 | 26 | // ::: ADD NEW TODO ::: 27 | const addTodo = (text: string) => { 28 | const newTodo: Todo = { 29 | id: nanoid(), 30 | text, 31 | status: 'undone', 32 | } 33 | 34 | setTodos([...todos, newTodo]) 35 | } 36 | 37 | // ::: DELETE A TODO ::: 38 | const deleteTodo = (id: string) => { 39 | setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id)) 40 | } 41 | 42 | // ::: EDIT A TODO ::: 43 | const editTodo = (id: string, text: string) => { 44 | setTodos(prevTodos => { 45 | return prevTodos.map(todo => { 46 | if (todo.id === id) { 47 | return { ...todo, text } 48 | } 49 | return todo 50 | }) 51 | }) 52 | } 53 | 54 | // ::: UPDATE TODO STATUS ::: 55 | const updateTodoStatus = (id: string) => { 56 | setTodos(prevTodos => { 57 | return prevTodos.map(todo => { 58 | if (todo.id === id) { 59 | return { 60 | ...todo, 61 | status: todo.status === 'undone' ? 'completed' : 'undone', 62 | } 63 | } 64 | return todo 65 | }) 66 | }) 67 | } 68 | 69 | const value: TodoContextProps = { 70 | todos, 71 | addTodo, 72 | deleteTodo, 73 | editTodo, 74 | updateTodoStatus, 75 | } 76 | 77 | return ( 78 | {props.children} 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /src/context/index.ts: -------------------------------------------------------------------------------- 1 | export { TodoProvider } from './TodoContext' 2 | export { useTodo } from './useTodo' 3 | export type { Todo } from './TodoContext' 4 | -------------------------------------------------------------------------------- /src/context/useTodo.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react' 2 | import { TodoContext } from './TodoContext' 3 | 4 | export const useTodo = () => { 5 | const context = useContext(TodoContext) 6 | 7 | if (!context) { 8 | throw new Error('useTodo must be used within a TodoProvider') 9 | } 10 | 11 | return context 12 | } 13 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply text-gray-200 bg-dark-900; 7 | } 8 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | import { TodoProvider } from './context' 6 | 7 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 8 | 9 | 10 | 11 | 12 | , 13 | ) 14 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: { 6 | colors: { 7 | dark: { 8 | 900: "#242424", 9 | }, 10 | }, 11 | }, 12 | }, 13 | plugins: [], 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | --------------------------------------------------------------------------------