├── .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 |
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 | [](https://www.freecodecamp.org/)
26 | [](https://reactjs.org/)
27 | [](#)
28 | [](https://tailwindcss.com/)
29 | [](https://www.framer.com/api/motion/)
30 |
31 |
32 |
33 |
34 |
35 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------